From 8627544946ac8f7ee32416a1b20dd671ab451efe Mon Sep 17 00:00:00 2001 From: david_bai Date: Sat, 13 Sep 2025 11:09:06 +0800 Subject: [PATCH 01/10] chore:Exit the room even if it is in transit --- .../ClipboardApp/FileListDisplay.tsx | 2 +- .../ClipboardApp/RetrieveTabPanel.tsx | 28 +-- .../components/ClipboardApp/SendTabPanel.tsx | 11 +- frontend/constants/messages/de.ts | 2 + frontend/constants/messages/en.ts | 2 + frontend/constants/messages/es.ts | 2 + frontend/constants/messages/fr.ts | 2 + frontend/constants/messages/ja.ts | 2 + frontend/constants/messages/ko.ts | 2 + frontend/constants/messages/zh.ts | 2 + frontend/hooks/useFileTransferHandler.ts | 6 +- frontend/hooks/useRoomManager.ts | 31 ++- frontend/lib/fileReceiver.ts | 194 +++++++++++++----- frontend/lib/fileSender.ts | 5 + .../lib/transfer/FileTransferOrchestrator.ts | 42 +++- frontend/lib/transfer/MessageHandler.ts | 5 +- frontend/lib/transfer/ProgressTracker.ts | 5 +- frontend/lib/transfer/StateManager.ts | 36 ++-- frontend/lib/transfer/index.ts | 7 +- frontend/lib/webrtcService.ts | 158 +++++++++++--- frontend/lib/webrtc_Initiator.ts | 2 +- frontend/lib/webrtc_base.ts | 62 ++++-- frontend/stores/fileTransferStore.ts | 9 +- frontend/types/messages.ts | 2 + 24 files changed, 451 insertions(+), 168 deletions(-) diff --git a/frontend/components/ClipboardApp/FileListDisplay.tsx b/frontend/components/ClipboardApp/FileListDisplay.tsx index feea035..435dc79 100644 --- a/frontend/components/ClipboardApp/FileListDisplay.tsx +++ b/frontend/components/ClipboardApp/FileListDisplay.tsx @@ -282,7 +282,7 @@ const FileListDisplay: React.FC = ({ } else { if (developmentEnv === "true") { postLogToBackend( - `[Firefox Debug] Skipping download logic - isSaveToDisk: ${isSaveToDisk}, onDownload: ${!!onDownload}` + `Skipping download logic - isSaveToDisk: ${isSaveToDisk}, onDownload: ${!!onDownload}` ); } } diff --git a/frontend/components/ClipboardApp/RetrieveTabPanel.tsx b/frontend/components/ClipboardApp/RetrieveTabPanel.tsx index 73a22bc..989223c 100644 --- a/frontend/components/ClipboardApp/RetrieveTabPanel.tsx +++ b/frontend/components/ClipboardApp/RetrieveTabPanel.tsx @@ -1,7 +1,6 @@ import React, { useCallback } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; -import RichTextEditor from "@/components/Editor/RichTextEditor"; import { ReadClipboardButton, WriteClipboardButton, @@ -9,7 +8,6 @@ import { import FileListDisplay from "@/components/ClipboardApp/FileListDisplay"; import type { Messages } from "@/types/messages"; import type { FileMeta } from "@/types/webrtc"; -import type { ProgressState } from "@/hooks/useWebRTCConnection"; // Assuming this type is exported import { useFileTransferStore } from "@/stores/fileTransferStore"; @@ -53,7 +51,7 @@ export function RetrieveTabPanel({ retrieveMessage, handleLeaveRoom, }: RetrieveTabPanelProps) { - // 从 store 中获取状态 + // Get the status from the store const { retrieveRoomStatusText, retrieveRoomIdInput, @@ -61,35 +59,25 @@ export function RetrieveTabPanel({ retrievedFileMetas, receiveProgress, isAnyFileTransferring, - senderDisconnected, isReceiverInRoom, } = useFileTransferStore(); const onLocationPick = useCallback(async (): Promise => { if (!messages) return false; // Should not happen if panel is rendered if (!window.showDirectoryPicker) { - putMessageInMs( - messages.text.ClipboardApp.pickSaveUnsupported, - false - ); + putMessageInMs(messages.text.ClipboardApp.pickSaveUnsupported, false); return false; } if (!window.confirm(messages.text.ClipboardApp.pickSaveMsg)) return false; try { const directoryHandle = await window.showDirectoryPicker(); await setReceiverDirectoryHandle(directoryHandle); - putMessageInMs( - messages.text.ClipboardApp.pickSaveSuccess, - false - ); + putMessageInMs(messages.text.ClipboardApp.pickSaveSuccess, false); return true; } catch (err: any) { if (err.name !== "AbortError") { console.error("Failed to set up folder receive:", err); - putMessageInMs( - messages.text.ClipboardApp.pickSaveError, - false - ); + putMessageInMs(messages.text.ClipboardApp.pickSaveError, false); } return false; } @@ -148,12 +136,14 @@ export function RetrieveTabPanel({ {messages.text.ClipboardApp.html.joinRoom_dis} diff --git a/frontend/components/ClipboardApp/SendTabPanel.tsx b/frontend/components/ClipboardApp/SendTabPanel.tsx index 5f1da2a..6845c88 100644 --- a/frontend/components/ClipboardApp/SendTabPanel.tsx +++ b/frontend/components/ClipboardApp/SendTabPanel.tsx @@ -11,7 +11,6 @@ import FileListDisplay from "@/components/ClipboardApp/FileListDisplay"; import AnimatedButton from "@/components/ui/AnimatedButton"; import type { Messages } from "@/types/messages"; import type { CustomFile, FileMeta } from "@/types/webrtc"; -import type { ProgressState } from "@/hooks/useWebRTCConnection"; import { useFileTransferStore } from "@/stores/fileTransferStore"; @@ -55,7 +54,7 @@ export function SendTabPanel({ currentValidatedShareRoomId, handleLeaveSenderRoom, }: SendTabPanelProps) { - // 从 store 中获取状态 + // Get the status from the store const { shareContent, sendFiles, @@ -212,12 +211,14 @@ export function SendTabPanel({ {messages.text.ClipboardApp.html.SyncSending_dis} diff --git a/frontend/constants/messages/de.ts b/frontend/constants/messages/de.ts index 99e16d1..e7c70cd 100644 --- a/frontend/constants/messages/de.ts +++ b/frontend/constants/messages/de.ts @@ -316,6 +316,8 @@ export const de: Messages = { noFilesForFolderMsg: "Keine Dateien im Ordner '{folderName}' gefunden.", zipError: "Fehler beim Erstellen der ZIP-Datei.", fileNotFoundMsg: "Datei '{fileName}' zum Herunterladen nicht gefunden.", + confirmLeaveWhileTransferring: "Dateien werden derzeit übertragen. Das Verlassen wird die Übertragung unterbrechen. Sind Sie sicher?", + leaveWhileTransferringSuccess: "Raum verlassen, Übertragung unterbrochen", html: { senderTab: "Senden", retrieveTab: "Abrufen", diff --git a/frontend/constants/messages/en.ts b/frontend/constants/messages/en.ts index cf2fa07..5d7ad00 100644 --- a/frontend/constants/messages/en.ts +++ b/frontend/constants/messages/en.ts @@ -308,6 +308,8 @@ export const en: Messages = { noFilesForFolderMsg: "No files found for folder '{folderName}'.", zipError: "Error creating ZIP.", fileNotFoundMsg: "File '{fileName}' not found for download.", + confirmLeaveWhileTransferring: "Files are currently transferring. Leaving will interrupt the transfer. Are you sure?", + leaveWhileTransferringSuccess: "Left room, transfer interrupted", html: { senderTab: "Send", retrieveTab: "Retrieve", diff --git a/frontend/constants/messages/es.ts b/frontend/constants/messages/es.ts index ae98104..8e605a2 100644 --- a/frontend/constants/messages/es.ts +++ b/frontend/constants/messages/es.ts @@ -310,6 +310,8 @@ export const es: Messages = { "No se encontraron archivos en la carpeta '{folderName}'.", zipError: "Error al crear el archivo ZIP.", fileNotFoundMsg: "Archivo '{fileName}' no encontrado para descargar.", + confirmLeaveWhileTransferring: "Los archivos se están transfiriendo actualmente. Salir interrumpirá la transferencia. ¿Estás seguro?", + leaveWhileTransferringSuccess: "Saliste de la sala, transferencia interrumpida", html: { senderTab: "Enviar", retrieveTab: "Recuperar", diff --git a/frontend/constants/messages/fr.ts b/frontend/constants/messages/fr.ts index c26b68f..63de6c4 100644 --- a/frontend/constants/messages/fr.ts +++ b/frontend/constants/messages/fr.ts @@ -318,6 +318,8 @@ export const fr: Messages = { zipError: "Erreur lors de la création du fichier ZIP.", fileNotFoundMsg: "Fichier '{fileName}' introuvable pour le téléchargement.", + confirmLeaveWhileTransferring: "Des fichiers sont actuellement en cours de transfert. Quitter interrompra le transfert. Êtes-vous sûr?", + leaveWhileTransferringSuccess: "Salle quittée, transfert interrompu", html: { senderTab: "Envoyer", retrieveTab: "Récupérer", diff --git a/frontend/constants/messages/ja.ts b/frontend/constants/messages/ja.ts index 12b943d..fa27900 100644 --- a/frontend/constants/messages/ja.ts +++ b/frontend/constants/messages/ja.ts @@ -302,6 +302,8 @@ export const ja: Messages = { noFilesForFolderMsg: "フォルダ '{folderName}' にファイルが見つかりません。", zipError: "ZIP の作成中にエラーが発生しました。", fileNotFoundMsg: "ダウンロードするファイル '{fileName}' が見つかりません。", + confirmLeaveWhileTransferring: "現在ファイルが転送中です。退出すると転送が中断されます。よろしいですか?", + leaveWhileTransferringSuccess: "ルームを退出しました。転送が中断されました", html: { senderTab: "送信", retrieveTab: "取得", diff --git a/frontend/constants/messages/ko.ts b/frontend/constants/messages/ko.ts index 1c29a15..68982f0 100644 --- a/frontend/constants/messages/ko.ts +++ b/frontend/constants/messages/ko.ts @@ -300,6 +300,8 @@ export const ko: Messages = { noFilesForFolderMsg: "폴더 '{folderName}'에서 파일을 찾을 수 없습니다.", zipError: "ZIP 파일 생성 중 오류가 발생했습니다.", fileNotFoundMsg: "다운로드할 파일 '{fileName}'을(를) 찾을 수 없습니다.", + confirmLeaveWhileTransferring: "현재 파일이 전송 중입니다. 나가면 전송이 중단됩니다. 확실합니까?", + leaveWhileTransferringSuccess: "방을 나갔습니다. 전송이 중단되었습니다", html: { senderTab: "보내기", retrieveTab: "검색", diff --git a/frontend/constants/messages/zh.ts b/frontend/constants/messages/zh.ts index 1adbf40..d0240e1 100644 --- a/frontend/constants/messages/zh.ts +++ b/frontend/constants/messages/zh.ts @@ -287,6 +287,8 @@ export const zh: Messages = { noFilesForFolderMsg: "在文件夹 '{folderName}' 中未找到文件。", zipError: "创建 ZIP 文件时出错。", fileNotFoundMsg: "未找到要下载的文件 '{fileName}'。", + confirmLeaveWhileTransferring: "当前有文件正在传输,退出将中断传输。确定要退出吗?", + leaveWhileTransferringSuccess: "已退出房间,传输已中断", html: { senderTab: "发送", retrieveTab: "接收", diff --git a/frontend/hooks/useFileTransferHandler.ts b/frontend/hooks/useFileTransferHandler.ts index 7029003..c845d41 100644 --- a/frontend/hooks/useFileTransferHandler.ts +++ b/frontend/hooks/useFileTransferHandler.ts @@ -117,14 +117,14 @@ export function useFileTransferHandler({ // Check if file is empty if (fileToDownload.size === 0) { postLogToBackend( - `[Firefox Debug] ERROR: File has 0 size! This explains the 0-byte download.` + `ERROR: File has 0 size! This explains the 0-byte download.` ); } // Check if file is a valid Blob if (!(fileToDownload instanceof Blob)) { postLogToBackend( - `[Firefox Debug] WARNING: File is not a Blob object, type: ${typeof fileToDownload}` + `WARNING: File is not a Blob object, type: ${typeof fileToDownload}` ); } @@ -134,7 +134,7 @@ export function useFileTransferHandler({ // Debug log: Record the case where file is not found const availableFileNames = latestRetrievedFiles.map((f) => f.name); postLogToBackend( - `[Firefox Debug] File NOT found! Looking for: "${ + `File NOT found! Looking for: "${ meta.name }", Available files: [${availableFileNames.join(", ")}]` ); diff --git a/frontend/hooks/useRoomManager.ts b/frontend/hooks/useRoomManager.ts index 3386809..2aafcf9 100644 --- a/frontend/hooks/useRoomManager.ts +++ b/frontend/hooks/useRoomManager.ts @@ -36,6 +36,7 @@ export function useRoomManager({ senderDisconnected, isSenderInRoom, isReceiverInRoom, + isAnyFileTransferring, setShareRoomId, setInitShareRoomId, setShareLink, @@ -150,6 +151,12 @@ export function useRoomManager({ const handleLeaveReceiverRoom = useCallback(async () => { if (!messages) return; + // Check if files are transferring and show confirmation + if (isAnyFileTransferring) { + const confirmed = window.confirm(messages.text.ClipboardApp.confirmLeaveWhileTransferring); + if (!confirmed) return; + } + try { // Call backend API to leave room if (webrtcService.receiver.roomId && webrtcService.receiver.peerId) { @@ -159,9 +166,12 @@ export function useRoomManager({ ); } - putMessageInMs(messages.text.ClipboardApp.roomStatus.leftRoomMsg, false); + const message = isAnyFileTransferring + ? messages.text.ClipboardApp.leaveWhileTransferringSuccess + : messages.text.ClipboardApp.roomStatus.leftRoomMsg; + putMessageInMs(message, false); - // Reset receiver state + // Reset receiver state (clears all as per requirement) resetReceiverState(); // Clean up WebRTC connection @@ -170,7 +180,7 @@ export function useRoomManager({ console.error("[RoomManager] Receiver failed to leave room:", error); putMessageInMs("Failed to leave room", true); } - }, [messages, putMessageInMs, resetReceiverState]); + }, [messages, putMessageInMs, resetReceiverState, isAnyFileTransferring]); // Sender reset app state const resetSenderAppState = useCallback(async () => { @@ -195,6 +205,12 @@ export function useRoomManager({ const handleLeaveSenderRoom = useCallback(async () => { if (!messages) return; + // Check if files are transferring and show confirmation + if (isAnyFileTransferring) { + const confirmed = window.confirm(messages.text.ClipboardApp.confirmLeaveWhileTransferring); + if (!confirmed) return; + } + try { // Call backend API to leave room if (webrtcService.sender.roomId && webrtcService.sender.peerId) { @@ -204,15 +220,18 @@ export function useRoomManager({ ); } - putMessageInMs(messages.text.ClipboardApp.roomStatus.leftRoomMsg, true); + const message = isAnyFileTransferring + ? messages.text.ClipboardApp.leaveWhileTransferringSuccess + : messages.text.ClipboardApp.roomStatus.leftRoomMsg; + putMessageInMs(message, true); - // Reset sender state and get new room ID + // Reset sender state and get new room ID (keeps files as per requirement) await resetSenderAppState(); } catch (error) { console.error("[RoomManager] Sender failed to leave room:", error); putMessageInMs("Failed to leave room", true); } - }, [messages, putMessageInMs, resetSenderAppState]); + }, [messages, putMessageInMs, resetSenderAppState, isAnyFileTransferring]); // Room ID input processing const processRoomIdInput = useCallback( diff --git a/frontend/lib/fileReceiver.ts b/frontend/lib/fileReceiver.ts index 991a57b..951a766 100644 --- a/frontend/lib/fileReceiver.ts +++ b/frontend/lib/fileReceiver.ts @@ -80,28 +80,46 @@ class SequencedDiskWriter { * Write current chunk and attempt to sequentially write subsequent chunks */ private async flushSequentialChunks(firstChunk: ArrayBuffer): Promise { - // Write current chunk - await this.stream.write(firstChunk); - this.totalWritten += firstChunk.byteLength; + let flushCount = 0; // 声明移到外部 - if (developmentEnv === "true") { - postLogToBackend( - `[DEBUG] ✓ DISK_WRITE chunk #${this.nextWriteIndex} - ${firstChunk.byteLength} bytes, total: ${this.totalWritten}` - ); - } + try { + // Write current chunk + await this.stream.write(firstChunk); + this.totalWritten += firstChunk.byteLength; - this.nextWriteIndex++; + if (developmentEnv === "true") { + postLogToBackend( + `[DEBUG] ✓ DISK_WRITE chunk #${this.nextWriteIndex} - ${firstChunk.byteLength} bytes, total: ${this.totalWritten}` + ); + } - // Try to sequentially write chunks from buffer - let flushCount = 0; - while (this.writeQueue.has(this.nextWriteIndex)) { - const chunk = this.writeQueue.get(this.nextWriteIndex)!; - await this.stream.write(chunk); - this.totalWritten += chunk.byteLength; - this.writeQueue.delete(this.nextWriteIndex); - - flushCount++; this.nextWriteIndex++; + + // Try to sequentially write chunks from buffer + while (this.writeQueue.has(this.nextWriteIndex)) { + const chunk = this.writeQueue.get(this.nextWriteIndex)!; + await this.stream.write(chunk); + this.totalWritten += chunk.byteLength; + this.writeQueue.delete(this.nextWriteIndex); + + flushCount++; + this.nextWriteIndex++; + } + } catch (error) { + // 🔒 防御性处理:如果流已关闭,静默忽略 + const errorMessage = + error instanceof Error ? error.message : String(error); + if ( + errorMessage.includes("closing writable stream") || + errorMessage.includes("stream is closed") + ) { + console.log( + `[SequencedDiskWriter] Stream closed during write - ignoring remaining chunks` + ); + return; + } + // 重新抛出其他类型的错误 + throw error; } if (flushCount > 0) { @@ -117,27 +135,44 @@ class SequencedDiskWriter { * Force refresh the earliest chunk to release buffer space */ private async forceFlushOldest(): Promise { - if (this.writeQueue.size === 0) return; + try { + if (this.writeQueue.size === 0) return; - const oldestIndex = Math.min(...Array.from(this.writeQueue.keys())); - const chunk = this.writeQueue.get(oldestIndex)!; + const oldestIndex = Math.min(...Array.from(this.writeQueue.keys())); + const chunk = this.writeQueue.get(oldestIndex)!; - // Warning: Unordered write - if (developmentEnv === "true") { - postLogToBackend( - `[DEBUG] ⚠️ FORCE_FLUSH out-of-order chunk #${oldestIndex} (expected #${this.nextWriteIndex})` - ); + // Warning: Unordered write + if (developmentEnv === "true") { + postLogToBackend( + `[DEBUG] ⚠️ FORCE_FLUSH out-of-order chunk #${oldestIndex} (expected #${this.nextWriteIndex})` + ); + } + + // Use seek to write at the correct position (fallback handling) + const fileOffset = oldestIndex * 65536; // Assume each chunk is 64KB + await this.stream.seek(fileOffset); + await this.stream.write(chunk); + this.writeQueue.delete(oldestIndex); + + // Return to current position + const currentOffset = this.nextWriteIndex * 65536; + await this.stream.seek(currentOffset); + } catch (error) { + // 🔒 防御性处理:如果流已关闭,静默忽略 + const errorMessage = + error instanceof Error ? error.message : String(error); + if ( + errorMessage.includes("closing writable stream") || + errorMessage.includes("stream is closed") + ) { + console.log( + `[SequencedDiskWriter] Stream closed during write - ignoring remaining chunks` + ); + return; + } + // 重新抛出其他类型的错误 + throw error; } - - // Use seek to write at the correct position (fallback handling) - const fileOffset = oldestIndex * 65536; // Assume each chunk is 64KB - await this.stream.seek(fileOffset); - await this.stream.write(chunk); - this.writeQueue.delete(oldestIndex); - - // Return to current position - const currentOffset = this.nextWriteIndex * 65536; - await this.stream.seek(currentOffset); } /** @@ -159,19 +194,39 @@ class SequencedDiskWriter { * Close and clean up resources */ async close(): Promise { - // Try to flush all remaining chunks - const remainingIndexes = Array.from(this.writeQueue.keys()).sort( - (a, b) => a - b - ); - for (const chunkIndex of remainingIndexes) { - const chunk = this.writeQueue.get(chunkIndex)!; - const fileOffset = chunkIndex * 65536; - await this.stream.seek(fileOffset); - await this.stream.write(chunk); - if (developmentEnv === "true") { - postLogToBackend( - `[DEBUG] 💾 FINAL_FLUSH chunk #${chunkIndex} at cleanup` + try { + // Try to flush all remaining chunks + const remainingIndexes = Array.from(this.writeQueue.keys()).sort( + (a, b) => a - b + ); + for (const chunkIndex of remainingIndexes) { + const chunk = this.writeQueue.get(chunkIndex)!; + const fileOffset = chunkIndex * 65536; + await this.stream.seek(fileOffset); + await this.stream.write(chunk); + if (developmentEnv === "true") { + postLogToBackend( + `[DEBUG] 💾 FINAL_FLUSH chunk #${chunkIndex} at cleanup` + ); + } + } + } catch (error) { + // 🔒 防御性处理:关闭时如果流已不可写,静默处理 + const errorMessage = + error instanceof Error ? error.message : String(error); + if ( + errorMessage.includes("closing writable stream") || + errorMessage.includes("stream is closed") + ) { + console.log( + `[SequencedDiskWriter] Stream closed during final flush - data may be incomplete` ); + } else { + console.warn( + `[SequencedDiskWriter] Error during final flush:`, + errorMessage + ); + throw error; // 重新抛出其他错误 } } @@ -594,7 +649,7 @@ class FileReceiver { const handler = this.fileHandlers[parsedData.type as keyof FileHandlers]; if (handler) { - await handler(parsedData, peerId); + await handler(parsedData as any, peerId); } else { console.warn( `[DEBUG] ⚠️ FileReceiver Handler not found: ${parsedData.type}` @@ -706,7 +761,15 @@ class FileReceiver { } const { chunkMeta, chunkData } = parsed; - const reception = this.activeFileReception!; + + // 🔒 防御性检查:确保文件接收还在活跃状态 + const reception = this.activeFileReception; + if (!reception) { + console.log( + `[FileReceiver] Ignoring chunk ${chunkMeta.chunkIndex} - file reception already closed` + ); + return; + } // Verify fileId match if (chunkMeta.fileId !== reception.meta.fileId) { @@ -1076,7 +1139,9 @@ class FileReceiver { } // endregion - public gracefulShutdown(): void { + public gracefulShutdown(reason: string = "CONNECTION_LOST"): void { + this.log("log", `Graceful shutdown initiated: ${reason}`); + if (this.activeFileReception?.sequencedWriter) { this.log( "log", @@ -1115,6 +1180,33 @@ class FileReceiver { this.activeFileReception = null; this.activeStringReception = null; this.currentFolderName = null; + + this.log("log", "Graceful shutdown completed"); + } + + /** + * Force reset all internal states - used when rejoining rooms + */ + public forceReset(): void { + this.log("log", "Force resetting FileReceiver state"); + + // Close any active streams first + if (this.activeFileReception?.sequencedWriter) { + this.activeFileReception.sequencedWriter.close().catch(console.error); + } + if (this.activeFileReception?.writeStream) { + this.activeFileReception.writeStream.close().catch(console.error); + } + + // Clear all states + this.pendingFilesMeta.clear(); + this.folderProgresses = {}; + this.saveType = {}; + this.activeFileReception = null; + this.activeStringReception = null; + this.currentFolderName = null; + + this.log("log", "FileReceiver state force reset completed"); } } diff --git a/frontend/lib/fileSender.ts b/frontend/lib/fileSender.ts index 57b9779..6ac53e1 100644 --- a/frontend/lib/fileSender.ts +++ b/frontend/lib/fileSender.ts @@ -41,6 +41,11 @@ class FileSender { return this.orchestrator.getTransferStats(peerId); } + public handlePeerReconnection(peerId: string): void { + this.orchestrator.handlePeerReconnection(peerId); + console.log(`[FileSender] Handled peer reconnection for ${peerId}`); + } + public cleanup(): void { return this.orchestrator.cleanup(); } diff --git a/frontend/lib/transfer/FileTransferOrchestrator.ts b/frontend/lib/transfer/FileTransferOrchestrator.ts index a33e717..5d58dc2 100644 --- a/frontend/lib/transfer/FileTransferOrchestrator.ts +++ b/frontend/lib/transfer/FileTransferOrchestrator.ts @@ -40,9 +40,6 @@ export class FileTransferOrchestrator implements MessageHandlerDelegate { this.log("log", "FileTransferOrchestrator initialized"); } - - // ===== Public API - Simplified interface ===== - /** * 🎯 Send file metadata */ @@ -168,7 +165,6 @@ export class FileTransferOrchestrator implements MessageHandlerDelegate { ): 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; @@ -182,7 +178,6 @@ export class FileTransferOrchestrator implements MessageHandlerDelegate { bufferQueue: [], isReading: false, }); - // Initialize progress statistics const currentSent = this.stateManager.getFileBytesSent(peerId, fileId); this.stateManager.updateFileBytesSent(peerId, fileId, offset - currentSent); @@ -338,9 +333,23 @@ export class FileTransferOrchestrator implements MessageHandlerDelegate { * ⏳ Wait for transfer completion confirmation */ private async waitForTransferComplete(peerId: string): Promise { - const peerState = this.stateManager.getPeerState(peerId); - while (peerState?.isSending) { - await new Promise((resolve) => setTimeout(resolve, 50)); + 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)); } } @@ -413,6 +422,19 @@ export class FileTransferOrchestrator implements MessageHandlerDelegate { return stats; } + /** + * 🔄 Handle peer reconnection + */ + public handlePeerReconnection(peerId: string): void { + // Clear all transfer states for this peer + this.stateManager.clearPeerState(peerId); + if (developmentEnv === "true") + this.log( + "log", + `Successfully reset transfer state for reconnected peer ${peerId}` + ); + } + /** * 🧹 Clean up all resources */ @@ -421,7 +443,7 @@ export class FileTransferOrchestrator implements MessageHandlerDelegate { this.networkTransmitter.cleanup(); this.progressTracker.cleanup(); this.messageHandler.cleanup(); - - this.log("log", "FileTransferOrchestrator cleaned up"); + if (developmentEnv === "true") + this.log("log", "FileTransferOrchestrator cleaned up"); } } diff --git a/frontend/lib/transfer/MessageHandler.ts b/frontend/lib/transfer/MessageHandler.ts index 5efda0d..61e8d57 100644 --- a/frontend/lib/transfer/MessageHandler.ts +++ b/frontend/lib/transfer/MessageHandler.ts @@ -2,7 +2,7 @@ import { WebRTCMessage, FileRequest, FileReceiveComplete, - FolderReceiveComplete, + FolderReceiveComplete } from "@/types/webrtc"; import { StateManager } from "./StateManager"; import { postLogToBackend } from "@/app/config/api"; @@ -172,6 +172,7 @@ export class MessageHandler { * 🧹 Clean up resources */ public cleanup(): void { - postLogToBackend("[DEBUG] 🧹 MessageHandler cleaned up"); + if (developmentEnv === "true") + postLogToBackend("[DEBUG] 🧹 MessageHandler cleaned up"); } } diff --git a/frontend/lib/transfer/ProgressTracker.ts b/frontend/lib/transfer/ProgressTracker.ts index 14cd129..18d0d06 100644 --- a/frontend/lib/transfer/ProgressTracker.ts +++ b/frontend/lib/transfer/ProgressTracker.ts @@ -1,7 +1,7 @@ import { SpeedCalculator } from "@/lib/speedCalculator"; import { StateManager } from "./StateManager"; import { postLogToBackend } from "@/app/config/api"; - +const developmentEnv = process.env.NEXT_PUBLIC_development!; /** * 🚀 Progress callback type definition */ @@ -225,6 +225,7 @@ export class ProgressTracker { */ cleanup(): void { // SpeedCalculator internally automatically cleans up expired data - postLogToBackend("[DEBUG] 🧹 ProgressTracker cleaned up"); + if (developmentEnv === "true") + postLogToBackend("[DEBUG] 🧹 ProgressTracker cleaned up"); } } diff --git a/frontend/lib/transfer/StateManager.ts b/frontend/lib/transfer/StateManager.ts index 6473f5c..654de6b 100644 --- a/frontend/lib/transfer/StateManager.ts +++ b/frontend/lib/transfer/StateManager.ts @@ -1,16 +1,6 @@ import { PeerState, CustomFile, FolderMeta } from "@/types/webrtc"; // Simplified version no longer depends on TransferConfig's complex configuration -/** - * 🚀 Network performance monitoring metrics interface - */ -export interface NetworkPerformanceMetrics { - avgClearingRate: number; // Average network clearing speed KB/s - optimalThreshold: number; // Dynamically optimized threshold - avgWaitTime: number; // Average waiting time - sampleCount: number; // Sample count -} - /** * 🚀 State management class * Centrally manages all transfer-related state data @@ -19,7 +9,6 @@ export class StateManager { private peerStates = new Map(); private pendingFiles = new Map(); private pendingFolderMeta: Record = {}; - private networkPerformance = new Map(); // ===== Peer state management ===== @@ -62,11 +51,10 @@ export class StateManager { } /** - * Remove peer state (when peer disconnects) + * Clear peer state immediately (for graceful disconnect) */ - public removePeerState(peerId: string): void { + public clearPeerState(peerId: string): void { this.peerStates.delete(peerId); - this.networkPerformance.delete(peerId); } // ===== File management ===== @@ -104,11 +92,15 @@ export class StateManager { /** * Add or update folder metadata */ - public addFileToFolder(folderName: string, fileId: string, fileSize: number): void { + public addFileToFolder( + folderName: string, + fileId: string, + fileSize: number + ): void { if (!this.pendingFolderMeta[folderName]) { this.pendingFolderMeta[folderName] = { totalSize: 0, fileIds: [] }; } - + const folderMeta = this.pendingFolderMeta[folderName]; if (!folderMeta.fileIds.includes(fileId)) { folderMeta.fileIds.push(fileId); @@ -134,7 +126,11 @@ export class StateManager { /** * Update file sent bytes */ - public updateFileBytesSent(peerId: string, fileId: string, bytes: number): void { + public updateFileBytesSent( + peerId: string, + fileId: string, + bytes: number + ): void { const peerState = this.getPeerState(peerId); if (!peerState.totalBytesSent[fileId]) { peerState.totalBytesSent[fileId] = 0; @@ -156,14 +152,14 @@ export class StateManager { public getFolderBytesSent(peerId: string, folderName: string): number { const folderMeta = this.getFolderMeta(folderName); const peerState = this.peerStates.get(peerId); - + if (!folderMeta || !peerState) return 0; let totalSent = 0; folderMeta.fileIds.forEach((fileId) => { totalSent += peerState.totalBytesSent[fileId] || 0; }); - + return totalSent; } @@ -176,7 +172,6 @@ export class StateManager { this.peerStates.clear(); this.pendingFiles.clear(); this.pendingFolderMeta = {}; - this.networkPerformance.clear(); } /** @@ -187,7 +182,6 @@ export class StateManager { peerCount: this.peerStates.size, pendingFileCount: this.pendingFiles.size, folderCount: Object.keys(this.pendingFolderMeta).length, - networkPerfCount: this.networkPerformance.size, }; } } diff --git a/frontend/lib/transfer/index.ts b/frontend/lib/transfer/index.ts index 41d0b22..a62f461 100644 --- a/frontend/lib/transfer/index.ts +++ b/frontend/lib/transfer/index.ts @@ -8,7 +8,6 @@ export { TransferConfig } from "./TransferConfig"; // State management export { StateManager } from "./StateManager"; -export type { NetworkPerformanceMetrics } from "./StateManager"; // High-performance file reading export { StreamingFileReader } from "./StreamingFileReader"; @@ -34,6 +33,8 @@ export { FileTransferOrchestrator } from "./FileTransferOrchestrator"; import WebRTC_Initiator from "../webrtc_Initiator"; import { FileTransferOrchestrator } from "./FileTransferOrchestrator"; -export function createFileTransferService(webrtcConnection: WebRTC_Initiator): FileTransferOrchestrator { +export function createFileTransferService( + webrtcConnection: WebRTC_Initiator +): FileTransferOrchestrator { return new FileTransferOrchestrator(webrtcConnection); -} \ No newline at end of file +} diff --git a/frontend/lib/webrtcService.ts b/frontend/lib/webrtcService.ts index d81af33..40e7564 100644 --- a/frontend/lib/webrtcService.ts +++ b/frontend/lib/webrtcService.ts @@ -8,8 +8,6 @@ import { config, } from "@/app/config/environment"; import { useFileTransferStore } from "@/stores/fileTransferStore"; -import type { CustomFile } from "@/types/webrtc"; -import { postLogToBackend } from "@/app/config/api"; class WebRTCService { public sender: WebRTC_Initiator; @@ -44,17 +42,21 @@ class WebRTCService { private initializeEventHandlers(): void { // Sender event handling this.sender.onConnectionStateChange = (state, peerId) => { + console.log(`[WebRTC Service] Sender connection state: ${state} for peer ${peerId}`); + useFileTransferStore.getState().setShareConnectionState(state as any); - useFileTransferStore - .getState() - .setSharePeerCount(this.sender.peerConnections.size); - if (state === "connected") { + // update share peer count + useFileTransferStore.getState().setSharePeerCount(this.sender.peerConnections.size); + console.log(`[WebRTC Service] Sender connected, peer count: ${this.sender.peerConnections.size}`); + this.fileSender.setProgressCallback((fileId, progress, speed) => { useFileTransferStore .getState() .updateSendProgress(fileId, peerId, { progress, speed }); }, peerId); + } else if (state === "failed" || state === "closed") { + this.handleConnectionDisconnect(peerId, true, `CONNECTION_${state.toUpperCase()}`); } }; @@ -65,49 +67,46 @@ class WebRTCService { }; this.sender.onPeerDisconnected = (peerId) => { - setTimeout(() => { - useFileTransferStore - .getState() - .setSharePeerCount(this.sender.peerConnections.size); - }, 0); + console.log(`[WebRTC Service] Sender peer disconnected: ${peerId}`); + this.handleConnectionDisconnect(peerId, true, "PEER_DISCONNECTED"); }; this.sender.onError = (error) => { console.error("[WebRTC Service] Sender error:", error.message); + // Clear all states on error + this.clearAllTransferProgress(); }; // Receiver event handling this.receiver.onConnectionStateChange = (state, peerId) => { + console.log(`[WebRTC Service] Receiver connection state: ${state} for peer ${peerId}`); + useFileTransferStore.getState().setRetrieveConnectionState(state as any); - useFileTransferStore - .getState() - .setRetrievePeerCount(this.receiver.peerConnections.size); if (state === "connected") { + // update retrieve peer count + useFileTransferStore.getState().setRetrievePeerCount(this.receiver.peerConnections.size); + console.log(`[WebRTC Service] Receiver connected, peer count: ${this.receiver.peerConnections.size}`); + this.fileReceiver.setProgressCallback((fileId, progress, speed) => { useFileTransferStore .getState() .updateReceiveProgress(fileId, peerId, { progress, speed }); }); - } else if (state === "failed" || state === "disconnected") { - const { isAnyFileTransferring } = useFileTransferStore.getState(); - if (isAnyFileTransferring) { - this.fileReceiver.gracefulShutdown(); - } + } else if (state === "failed" || state === "closed") { + this.handleConnectionDisconnect(peerId, false, `CONNECTION_${state.toUpperCase()}`); } }; this.receiver.onConnectionEstablished = (peerId) => { - const store = useFileTransferStore.getState(); + this.fileSender.handlePeerReconnection(peerId); useFileTransferStore.getState().setSenderDisconnected(false); useFileTransferStore.getState().setIsReceiverInRoom(true); }; this.receiver.onPeerDisconnected = (peerId) => { - const store = useFileTransferStore.getState(); - - useFileTransferStore.getState().setSenderDisconnected(true); - useFileTransferStore.getState().setRetrievePeerCount(0); + console.log(`[WebRTC Service] Receiver peer disconnected: ${peerId}`); + this.handleConnectionDisconnect(peerId, false, "PEER_DISCONNECTED"); }; this.fileReceiver.onStringReceived = (data) => { @@ -140,6 +139,12 @@ class WebRTCService { // Business methods public async joinRoom(roomId: string, isSender: boolean): Promise { + // Ensure clean state before joining + if (!isSender) { + // Force reset FileReceiver state to prevent "already in progress" errors + this.fileReceiver.forceReset(); + } + const peer = isSender ? this.sender : this.receiver; await peer.joinRoom(roomId, isSender); @@ -149,12 +154,17 @@ class WebRTCService { setInRoom(true); } + public async leaveRoom(isSender: boolean): Promise { if (isSender) { + // Clean up sender + this.fileSender.cleanup(); await this.sender.leaveRoomAndCleanup(); useFileTransferStore.getState().setIsSenderInRoom(false); useFileTransferStore.getState().setSharePeerCount(0); } else { + // Clean up receiver - force reset to ensure complete cleanup + this.fileReceiver.forceReset(); await this.receiver.leaveRoomAndCleanup(); useFileTransferStore.getState().setIsReceiverInRoom(false); useFileTransferStore.getState().setRetrievePeerCount(0); @@ -209,6 +219,106 @@ class WebRTCService { this.fileReceiver.gracefulShutdown(); } + private handleConnectionDisconnect(peerId: string, isSender: boolean, reason: string): void { + console.log(`[WebRTC Service] Connection disconnect: ${reason}, peer: ${peerId}, sender: ${isSender}`); + + // Immediately clean up the transfer status to avoid UI freezing + this.immediateTransferCleanup(peerId, isSender, reason); + + // update connection state + this.updateConnectionState(peerId, isSender); + } + + // Immediately clean up the transfer status + private immediateTransferCleanup(peerId: string, isSender: boolean, reason: string): void { + const store = useFileTransferStore.getState(); + + if (isSender) { + // Sender disconnected: clean up the sender related status + this.clearPeerTransferProgress(peerId, true); + } else { + // Receiver side: sender disconnected, need to clean up the receiver status + const { isAnyFileTransferring } = store; + + if (isAnyFileTransferring) { + console.log(`[WebRTC Service] Force cleaning receiver due to sender disconnect: ${reason}`); + + // Catch the error that gracefulShutdown may throw + try { + this.fileReceiver.gracefulShutdown(`SENDER_${reason}`); + } catch (error) { + console.log(`[WebRTC Service] Expected error during graceful shutdown:`, error instanceof Error ? error.message : String(error)); + } + } + + this.clearPeerTransferProgress(peerId, false); + } + } + + // update connection state + private updateConnectionState(peerId: string, isSender: boolean): void { + const store = useFileTransferStore.getState(); + + if (isSender) { + // Sender disconnected: clean up the sender related status + const currentShareCount = store.sharePeerCount; + store.setSharePeerCount(Math.max(0, currentShareCount - 1)); + console.log(`[WebRTC Service] Sender peer count: ${currentShareCount} → ${Math.max(0, currentShareCount - 1)}`); + } else { + // Receiver side: sender disconnected, need to clean up the receiver status + store.setRetrievePeerCount(0); + store.setSenderDisconnected(true); + console.log(`[WebRTC Service] Receiver peer count set to 0`); + } + } + + // Clear all transfer progress + private clearAllTransferProgress(): void { + const store = useFileTransferStore.getState(); + store.setSendProgress({}); + store.setReceiveProgress({}); + store.setIsAnyFileTransferring(false); + console.log(`[WebRTC Service] Cleared all transfer progress`); + } + + private clearPeerTransferProgress(peerId: string, isSender: boolean): void { + const store = useFileTransferStore.getState(); + const progressState = isSender ? store.sendProgress : store.receiveProgress; + + // Clear transfer progress for this peer + const newProgress = { ...progressState }; + Object.keys(newProgress).forEach((fileId) => { + if (newProgress[fileId][peerId]) { + delete newProgress[fileId][peerId]; + // If no other peers are transferring this file, remove the file record + if (Object.keys(newProgress[fileId]).length === 0) { + delete newProgress[fileId]; + } + } + }); + + if (isSender) { + store.setSendProgress(newProgress); + } else { + store.setReceiveProgress(newProgress); + } + + // Recalculate isAnyFileTransferring status + const allProgress = [ + ...Object.values(isSender ? newProgress : store.sendProgress), + ...Object.values(isSender ? store.receiveProgress : newProgress), + ]; + const hasActiveTransfers = allProgress.some((fileProgress: any) => { + return Object.values(fileProgress).some((progress: any) => { + return progress.progress > 0 && progress.progress < 1; + }); + }); + + if (!hasActiveTransfers) { + store.setIsAnyFileTransferring(false); + } + } + public async cleanup(): Promise { console.log("[WebRTC Service] Starting cleanup..."); try { diff --git a/frontend/lib/webrtc_Initiator.ts b/frontend/lib/webrtc_Initiator.ts index 52e1411..05ab205 100644 --- a/frontend/lib/webrtc_Initiator.ts +++ b/frontend/lib/webrtc_Initiator.ts @@ -85,7 +85,7 @@ export default class WebRTC_Initiator extends BaseWebRTC { this.dataChannels.set(peerId, dataChannel); } catch (error) { postLogToBackend( - `[Firefox Debug] Error creating DataChannel - peer: ${peerId}, error: ${error}` + `Error creating DataChannel - peer: ${peerId}, error: ${error}` ); this.fireError(`Error creating data channel for peer ${peerId}`, { error, diff --git a/frontend/lib/webrtc_base.ts b/frontend/lib/webrtc_base.ts index 790daa0..967b218 100644 --- a/frontend/lib/webrtc_base.ts +++ b/frontend/lib/webrtc_base.ts @@ -64,6 +64,8 @@ export default class BaseWebRTC { protected isPeerDisconnected: boolean; // Tracks P2P connection status protected reconnectionInProgress: boolean; // Prevents duplicate reconnections protected wakeLockManager: WakeLockManager; + // Graceful disconnect tracking + protected gracefullyDisconnectedPeers: Set; constructor(config: WebRTCConfig) { this.iceServers = config.iceServers; @@ -83,6 +85,7 @@ export default class BaseWebRTC { this.roomId = null; this.peerId = null; // Own ID this.isInRoom = false; // Whether the user has already joined a room + this.gracefullyDisconnectedPeers = new Set(); // Track peers that disconnected gracefully this.setupCommonSocketListeners(); this.isInitiator = false; @@ -378,24 +381,29 @@ export default class BaseWebRTC { event.data?.length || event.data?.size || event.data?.byteLength || 0; } - // postLogToBackend( - // `[Firefox Debug] DataChannel onmessage - peer: ${peerId}, dataType: ${dataType}, size: ${dataSize}` - // ); - if (this.onDataReceived) { this.onDataReceived(event.data, peerId); } }; dataChannel.onerror = (error) => { - this.log("error", `Data channel error for peer ${peerId}`, { error }); + // Check if this is a user-initiated disconnect (not a real error) + const isUserDisconnect = + error.error?.message?.includes("User-Initiated Abort") || + error.error?.message?.includes("Close called"); + + if (isUserDisconnect) { + this.log("log", `Data channel closed by user for peer ${peerId}`, { + error, + }); + } else { + this.log("error", `Data channel error for peer ${peerId}`, { error }); + } }; dataChannel.onclose = () => { if (developmentEnv === "true") { - postLogToBackend( - `[Firefox Debug] DataChannel closed for peer: ${peerId}` - ); + postLogToBackend(`DataChannel closed for peer: ${peerId}`); } this.log("log", `Data channel with ${peerId} closed.`); }; @@ -473,7 +481,7 @@ export default class BaseWebRTC { } } // Send to a specific peer - protected sendToPeer(data: any, peerId: string): boolean { + public sendToPeer(data: any, peerId: string): boolean { const dataChannel = this.dataChannels.get(peerId); if (dataChannel?.readyState === "open") { try { @@ -491,19 +499,22 @@ export default class BaseWebRTC { ? data.byteLength : 0; - // postLogToBackend(`[Firefox Debug] sendToPeer - type: ${dataType}, size: ${dataSize}, bufferedAmount: ${dataChannel.bufferedAmount}`); + if (developmentEnv === "true") + postLogToBackend( + `sendToPeer - type: ${dataType}, size: ${dataSize}, bufferedAmount: ${dataChannel.bufferedAmount}` + ); dataChannel.send(data); return true; } catch (error) { - postLogToBackend(`[Firefox Debug] sendToPeer error: ${error}`); + postLogToBackend(`sendToPeer error: ${error}`); this.log("error", `Error sending data to peer ${peerId}`, { error }); return false; } } postLogToBackend( - `[Firefox Debug] DataChannel not ready - peerId: ${peerId}, state: ${ + `DataChannel not ready - peerId: ${peerId}, state: ${ dataChannel?.readyState || "undefined" }` ); @@ -512,11 +523,29 @@ export default class BaseWebRTC { } protected retryDataSend(data: any, peerId: string): boolean { + // Check if peer has gracefully disconnected - no need to retry + if (this.gracefullyDisconnectedPeers.has(peerId)) { + this.log( + "log", + `Peer ${peerId} has gracefully disconnected, skipping retry` + ); + return false; + } + const maxRetries = 5; let retryCount = 0; let ret = false; const attemptSend = () => { + // Check again in case peer disconnected during retry + if (this.gracefullyDisconnectedPeers.has(peerId)) { + this.log( + "log", + `Peer ${peerId} gracefully disconnected during retry, stopping` + ); + return; + } + const dataChannel = this.dataChannels.get(peerId); if (dataChannel?.readyState === "open") { dataChannel.send(data); @@ -539,6 +568,14 @@ export default class BaseWebRTC { return ret; } + /** + * Mark a peer as gracefully disconnected to prevent unnecessary retries + */ + public markPeerGracefullyDisconnected(peerId: string): void { + this.gracefullyDisconnectedPeers.add(peerId); + this.log("log", `Marked peer ${peerId} as gracefully disconnected`); + } + protected async closeDataChannel(peerId: string): Promise { const dataChannel = this.dataChannels.get(peerId); if (dataChannel) { @@ -583,6 +620,7 @@ export default class BaseWebRTC { this.isPeerDisconnected = false; this.isSocketDisconnected = false; this.reconnectionInProgress = false; + this.gracefullyDisconnectedPeers.clear(); // Clear graceful disconnect tracking this.log( "log", diff --git a/frontend/stores/fileTransferStore.ts b/frontend/stores/fileTransferStore.ts index ce339f4..db1fb03 100644 --- a/frontend/stores/fileTransferStore.ts +++ b/frontend/stores/fileTransferStore.ts @@ -238,14 +238,7 @@ export const useFileTransferStore = create()((set, get) => ({ setRetrieveMessage: (message) => set({ retrieveMessage: message }), resetReceiverState: () => { - // 🔧 Clean up FileReceiver's internal state (via Service layer) - try { - const { webrtcService } = require("@/lib/webrtcService"); - webrtcService.fileReceiver.gracefulShutdown(); - } catch (error) { - console.warn(`[DEBUG] ⚠️ Failed to clean up FileReceiver state:`, error); - } - + // 🔧 Only reset Store state - FileReceiver cleanup is handled by webrtcService.leaveRoom() set({ retrievedContent: "", retrievedFiles: [], diff --git a/frontend/types/messages.ts b/frontend/types/messages.ts index 0dd61d5..bb6926e 100644 --- a/frontend/types/messages.ts +++ b/frontend/types/messages.ts @@ -275,6 +275,8 @@ export type ClipboardApp = { noFilesForFolderMsg?: string; zipError?: string; fileNotFoundMsg?: string; + confirmLeaveWhileTransferring: string; + leaveWhileTransferringSuccess: string; }; export type Home = { From 33f2f041ac4f07f6400861693508976cc1cc3f4e Mon Sep 17 00:00:00 2001 From: david_bai Date: Sun, 14 Sep 2025 07:35:34 +0800 Subject: [PATCH 02/10] fix:Try to fix the problem of incomplete file size in resumable download --- .../ClipboardApp/FileListDisplay.tsx | 8 +- frontend/lib/fileReceiver.ts | 302 ++++++++++++++---- frontend/lib/tracking.ts | 2 +- .../lib/transfer/FileTransferOrchestrator.ts | 32 +- frontend/lib/transfer/MessageHandler.ts | 6 +- frontend/lib/transfer/NetworkTransmitter.ts | 14 +- frontend/lib/transfer/ProgressTracker.ts | 4 +- frontend/lib/transfer/StreamingFileReader.ts | 75 ++++- frontend/lib/webrtc_Initiator.ts | 10 +- frontend/lib/webrtc_base.ts | 22 +- 10 files changed, 354 insertions(+), 121 deletions(-) diff --git a/frontend/components/ClipboardApp/FileListDisplay.tsx b/frontend/components/ClipboardApp/FileListDisplay.tsx index 435dc79..7d2943d 100644 --- a/frontend/components/ClipboardApp/FileListDisplay.tsx +++ b/frontend/components/ClipboardApp/FileListDisplay.tsx @@ -13,7 +13,7 @@ import type { Messages } from "@/types/messages"; import { useFileTransferStore } from "@/stores/fileTransferStore"; import { supportsAutoDownload } from "@/lib/browserUtils"; import { postLogToBackend } from "@/app/config/api"; -const developmentEnv = process.env.NEXT_PUBLIC_development!; +const developmentEnv = process.env.NODE_ENV; function formatFolderDis(template: string, num: number, size: string) { return template.replace("{num}", num.toString()).replace("{size}", size); @@ -261,7 +261,7 @@ const FileListDisplay: React.FC = ({ if (isAutoDownloadSupported) { // Browsers that support automatic downloads like Chrome: Download directly - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[Download Debug] Auto-downloading file: ${item.name}` ); @@ -269,7 +269,7 @@ const FileListDisplay: React.FC = ({ onDownload(item); } else { // Non-Chrome browsers: Set to save status, wait for user manual click - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[Download Debug] Setting pendingSave for non-Chrome browser: ${item.name}` ); @@ -280,7 +280,7 @@ const FileListDisplay: React.FC = ({ })); } } else { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `Skipping download logic - isSaveToDisk: ${isSaveToDisk}, onDownload: ${!!onDownload}` ); diff --git a/frontend/lib/fileReceiver.ts b/frontend/lib/fileReceiver.ts index 951a766..51992ce 100644 --- a/frontend/lib/fileReceiver.ts +++ b/frontend/lib/fileReceiver.ts @@ -22,7 +22,7 @@ import { EmbeddedChunkMeta, } from "@/types/webrtc"; import { postLogToBackend } from "@/app/config/api"; -const developmentEnv = process.env.NEXT_PUBLIC_development!; +const developmentEnv = process.env.NODE_ENV; /** * 🚀 Strict Sequential Buffering Writer - Optimizes large file disk I/O performance */ @@ -40,6 +40,18 @@ class SequencedDiskWriter { /**\n * Write a chunk, automatically managing order and buffering\n */ async writeChunk(chunkIndex: number, chunk: ArrayBuffer): Promise { + // 🔍 调试writeChunk调用 + if ( + developmentEnv === "development" && + (chunkIndex <= 5 || chunkIndex === this.nextWriteIndex) + ) { + postLogToBackend( + `[DEBUG-RESUME] 🎯 WriteChunk called - received:${chunkIndex}, expected:${ + this.nextWriteIndex + }, match:${chunkIndex === this.nextWriteIndex}` + ); + } + // 1. If it is the expected next chunk, write immediately if (chunkIndex === this.nextWriteIndex) { await this.flushSequentialChunks(chunk); @@ -50,26 +62,26 @@ class SequencedDiskWriter { if (chunkIndex > this.nextWriteIndex) { if (this.writeQueue.size < this.maxBufferSize) { this.writeQueue.set(chunkIndex, chunk); - if (developmentEnv === "true") { - postLogToBackend( - `[DEBUG] 📦 BUFFERED chunk #${chunkIndex} (waiting for #${this.nextWriteIndex}), queue: ${this.writeQueue.size}/${this.maxBufferSize}` - ); - } + // if (developmentEnv === "development") { + // postLogToBackend( + // `[DEBUG] 📦 BUFFERED chunk #${chunkIndex} (waiting for #${this.nextWriteIndex}), queue: ${this.writeQueue.size}/${this.maxBufferSize}` + // ); + // } } else { // Buffer full, forcing processing of the earliest chunk to free up space await this.forceFlushOldest(); this.writeQueue.set(chunkIndex, chunk); - if (developmentEnv === "true") { - postLogToBackend( - `[DEBUG] ⚠️ BUFFER_FULL, forced flush and buffered chunk #${chunkIndex}` - ); - } + // if (developmentEnv === "development") { + // postLogToBackend( + // `[DEBUG] ⚠️ BUFFER_FULL, forced flush and buffered chunk #${chunkIndex}` + // ); + // } } return; } // 3. If the chunk is expired, log a warning but ignore (already written) - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] ⚠️ DUPLICATE chunk #${chunkIndex} ignored (already written #${this.nextWriteIndex})` ); @@ -87,7 +99,7 @@ class SequencedDiskWriter { await this.stream.write(firstChunk); this.totalWritten += firstChunk.byteLength; - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] ✓ DISK_WRITE chunk #${this.nextWriteIndex} - ${firstChunk.byteLength} bytes, total: ${this.totalWritten}` ); @@ -123,7 +135,7 @@ class SequencedDiskWriter { } if (flushCount > 0) { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] 🔥 SEQUENTIAL_FLUSH ${flushCount} chunks, now at #${this.nextWriteIndex}, queue: ${this.writeQueue.size}` ); @@ -131,6 +143,13 @@ class SequencedDiskWriter { } } + /** + * Get the next expected write index + */ + get expectedIndex(): number { + return this.nextWriteIndex; + } + /** * Force refresh the earliest chunk to release buffer space */ @@ -142,11 +161,11 @@ class SequencedDiskWriter { const chunk = this.writeQueue.get(oldestIndex)!; // Warning: Unordered write - if (developmentEnv === "true") { - postLogToBackend( - `[DEBUG] ⚠️ FORCE_FLUSH out-of-order chunk #${oldestIndex} (expected #${this.nextWriteIndex})` - ); - } + // if (developmentEnv === "development") { + // postLogToBackend( + // `[DEBUG] ⚠️ FORCE_FLUSH out-of-order chunk #${oldestIndex} (expected #${this.nextWriteIndex})` + // ); + // } // Use seek to write at the correct position (fallback handling) const fileOffset = oldestIndex * 65536; // Assume each chunk is 64KB @@ -204,7 +223,7 @@ class SequencedDiskWriter { const fileOffset = chunkIndex * 65536; await this.stream.seek(fileOffset); await this.stream.write(chunk); - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] 💾 FINAL_FLUSH chunk #${chunkIndex} at cleanup` ); @@ -410,6 +429,38 @@ class FileReceiver { const receptionPromise = new Promise((resolve, reject) => { const expectedChunksCount = Math.ceil((fileInfo.size - offset) / 65536); // Calculate expected chunk count + // 🔍 调试expectedChunksCount计算 + if (developmentEnv === "development") { + const totalChunks = Math.ceil(fileInfo.size / 65536); + const startChunkIndex = Math.floor(offset / 65536); + const calculatedExpected = totalChunks - startChunkIndex; + + postLogToBackend(`[DEBUG-CHUNKS] File: ${fileInfo.name}`); + postLogToBackend( + `[DEBUG-CHUNKS] File size: ${fileInfo.size}, offset: ${offset}` + ); + postLogToBackend( + `[DEBUG-CHUNKS] Total chunks in file: ${totalChunks} (0-${ + totalChunks - 1 + })` + ); + postLogToBackend( + `[DEBUG-CHUNKS] Start chunk index: ${startChunkIndex}` + ); + postLogToBackend( + `[DEBUG-CHUNKS] Expected chunks calculation: (${fileInfo.size} - ${offset}) / 65536 = ${expectedChunksCount}` + ); + postLogToBackend( + `[DEBUG-CHUNKS] Alternative calculation: ${totalChunks} - ${startChunkIndex} = ${calculatedExpected}` + ); + + if (expectedChunksCount !== calculatedExpected) { + postLogToBackend( + `[DEBUG-CHUNKS] ⚠️ MISMATCH: ${expectedChunksCount} vs ${calculatedExpected}` + ); + } + } + this.activeFileReception = { meta: fileInfo, chunks: new Array(expectedChunksCount).fill(null), // 🚀 Initialize as an empty array arranged by index @@ -435,7 +486,7 @@ class FileReceiver { this.webrtcConnection.sendData(JSON.stringify(request), this.peerId); this.log("log", "Sent fileRequest", { request }); } else { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] ERROR: Cannot send fileRequest - no peerId available!` ); @@ -507,7 +558,7 @@ class FileReceiver { return true; }); - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] 📁 All files in folder completed - ${folderName}, files: ${completedFileIds.length}/${folderProgress.fileIds.length}` ); @@ -533,7 +584,7 @@ class FileReceiver { try { const arrayBuffer = await data.arrayBuffer(); if (data.size !== arrayBuffer.byteLength) { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] ⚠️ Blob size mismatch: ${data.size}→${arrayBuffer.byteLength}` ); @@ -541,7 +592,7 @@ class FileReceiver { } return arrayBuffer; } catch (error) { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend(`[DEBUG] ❌ Blob conversion failed: ${error}`); } return null; @@ -556,13 +607,13 @@ class FileReceiver { new Uint8Array(newArrayBuffer).set(uint8Array); return newArrayBuffer; } catch (error) { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend(`[DEBUG] ❌ TypedArray conversion failed: ${error}`); } return null; } } else { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] ❌ Unknown data type: ${Object.prototype.toString.call( data @@ -584,7 +635,7 @@ class FileReceiver { try { // 1. Check minimum packet length if (arrayBuffer.byteLength < 4) { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] ❌ Invalid embedded packet - too small: ${arrayBuffer.byteLength}` ); @@ -599,7 +650,7 @@ class FileReceiver { // 3. Verify packet integrity const expectedTotalLength = 4 + metaLength; if (arrayBuffer.byteLength < expectedTotalLength) { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] ❌ Incomplete embedded packet - expected: ${expectedTotalLength}, got: ${arrayBuffer.byteLength}` ); @@ -618,7 +669,7 @@ class FileReceiver { // 6. Verify chunk data size if (chunkData.byteLength !== chunkMeta.chunkSize) { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] ⚠️ Chunk size mismatch - meta: ${chunkMeta.chunkSize}, actual: ${chunkData.byteLength}` ); @@ -627,7 +678,7 @@ class FileReceiver { return { chunkMeta, chunkData }; } catch (error) { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] ❌ Failed to parse embedded packet: ${error}` ); @@ -664,7 +715,7 @@ class FileReceiver { if (arrayBuffer) { if (!this.activeFileReception) { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] ERROR: Received file chunk but no active file reception!` ); @@ -679,7 +730,7 @@ class FileReceiver { // 🚀 Unified processing: All data is processed as embedded packets await this.handleEmbeddedChunkPacket(arrayBuffer); } else { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] ERROR: Failed to convert binary data to ArrayBuffer` ); @@ -773,7 +824,7 @@ class FileReceiver { // Verify fileId match if (chunkMeta.fileId !== reception.meta.fileId) { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] ⚠️ FileId mismatch - expected: ${reception.meta.fileId}, got: ${chunkMeta.fileId}` ); @@ -781,44 +832,74 @@ class FileReceiver { return; } - // Update expected chunks count (may differ from initial estimate) + // 🔧 修复:续传时不要调整expectedChunksCount + // chunkMeta.totalChunks是文件总chunk数,但续传时我们只接收部分chunks if (chunkMeta.totalChunks !== reception.expectedChunksCount) { - if (developmentEnv === "true") { + if (developmentEnv === "development") { + const startChunkIndex = Math.floor(reception.initialOffset / 65536); + const calculatedExpected = chunkMeta.totalChunks - startChunkIndex; postLogToBackend( - `[DEBUG] ⚠️ Chunk count adjustment - expected: ${reception.expectedChunksCount}, actual: ${chunkMeta.totalChunks}` + `[DEBUG-CHUNKS] Chunk count info - fileTotal: ${chunkMeta.totalChunks}, currentExpected: ${reception.expectedChunksCount}, calculatedExpected: ${calculatedExpected}, startChunk: ${startChunkIndex}` ); - } - reception.expectedChunksCount = chunkMeta.totalChunks; - // Adjust chunks array size - if (reception.chunks.length < chunkMeta.totalChunks) { - const newChunks = new Array(chunkMeta.totalChunks).fill(null); - reception.chunks.forEach((chunk, index) => { - if (index < newChunks.length) newChunks[index] = chunk; - }); - reception.chunks = newChunks; + + // 🚫 不再调整expectedChunksCount,保持续传时的正确数量 + // reception.expectedChunksCount = chunkMeta.totalChunks; // 这行导致了问题! + + if (reception.expectedChunksCount !== calculatedExpected) { + postLogToBackend( + `[DEBUG-CHUNKS] ⚠️ Expected chunks mismatch, should be ${calculatedExpected}` + ); + } } } - // Store chunk by index - const chunkIndex = chunkMeta.chunkIndex; - if (chunkIndex >= 0 && chunkIndex < reception.chunks.length) { - reception.chunks[chunkIndex] = chunkData; - reception.chunkSequenceMap.set(chunkIndex, true); + // Store chunk by index - 🔧 修复:将绝对索引映射到相对索引 + const absoluteChunkIndex = chunkMeta.chunkIndex; // 发送端的绝对索引(如967-3650) + const startChunkIndex = Math.floor(reception.initialOffset / 65536); // 续传起始索引(如967) + const relativeChunkIndex = absoluteChunkIndex - startChunkIndex; // 在chunks数组中的相对索引(如0-2683) + + if (developmentEnv === "development" && absoluteChunkIndex <= 970) { + postLogToBackend( + `[DEBUG-CHUNKS] Index mapping - absolute:${absoluteChunkIndex}, start:${startChunkIndex}, relative:${relativeChunkIndex}, arraySize:${reception.chunks.length}` + ); + } + + if ( + relativeChunkIndex >= 0 && + relativeChunkIndex < reception.chunks.length + ) { + reception.chunks[relativeChunkIndex] = chunkData; + reception.chunkSequenceMap.set(absoluteChunkIndex, true); // 序列映射仍使用绝对索引 reception.receivedChunksCount++; // Update progress this.updateProgress(chunkData.byteLength); if (reception.sequencedWriter) { - // 🚀 Use strict sequential write management - await reception.sequencedWriter.writeChunk(chunkIndex, chunkData); + // 🔍 调试chunk接收匹配 (前5个和后5个chunks) + const lastFewChunks = + relativeChunkIndex >= reception.expectedChunksCount - 5; + if ( + developmentEnv === "development" && + (absoluteChunkIndex <= 970 || lastFewChunks) + ) { + postLogToBackend( + `[DEBUG-CHUNKS] 📦 Chunk #${absoluteChunkIndex} received - relative:${relativeChunkIndex}, size:${chunkData.byteLength}, writerExpects:${reception.sequencedWriter.expectedIndex}, isLastFew:${lastFewChunks}` + ); + } + + // 🚀 Use strict sequential write management - 使用绝对索引 + await reception.sequencedWriter.writeChunk( + absoluteChunkIndex, + chunkData + ); } } else { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( - `[DEBUG] ❌ Invalid chunk index - ${chunkIndex}, expected 0-${ - reception.chunks.length - 1 - }` + `[DEBUG-CHUNKS] ❌ Invalid relative chunk index - absolute:${absoluteChunkIndex}, relative:${relativeChunkIndex}, arraySize:${ + reception.chunks.length + }, expected:0-${reception.chunks.length - 1}` ); } } @@ -840,7 +921,8 @@ class FileReceiver { const currentTotalSize = reception.chunks.reduce((sum, chunk) => { return sum + (chunk instanceof ArrayBuffer ? chunk.byteLength : 0); }, 0); - const expectedSize = reception.meta.size; + // 🔧 修复:续传时应该比较的是剩余文件大小,不是整个文件大小 + const expectedSize = reception.meta.size - reception.initialOffset; // 🚀 Unified integrity check: sequential reception mode let sequencedCount = 0; @@ -850,10 +932,55 @@ class FileReceiver { } } const isSequencedComplete = sequencedCount === expectedChunks; - const sizeComplete = currentTotalSize >= expectedSize; const isDataComplete = isSequencedComplete && sizeComplete; + // 🔍 详细调试完成检查 (减少频率,只在关键时刻输出) + if ( + developmentEnv === "development" && + (isDataComplete || + sequencedCount % 500 === 0 || + sequencedCount > expectedChunks - 10) + ) { + // 检查最后几个chunks的状态 (显示相对索引) + const lastChunkIndex = expectedChunks - 1; + const lastFewChunks = []; + const startChunkIndex = Math.floor(reception.initialOffset / 65536); + + for (let i = Math.max(0, lastChunkIndex - 3); i <= lastChunkIndex; i++) { + const chunk = reception.chunks[i]; + const exists = chunk instanceof ArrayBuffer; + const size = exists ? (chunk as ArrayBuffer).byteLength : 0; + const absoluteIndex = startChunkIndex + i; // 对应的绝对索引 + lastFewChunks.push(`rel#${i}(abs#${absoluteIndex}):${exists}(${size})`); + } + + postLogToBackend( + `[DEBUG-COMPLETE] Check completion - file:${reception.meta.name}` + ); + postLogToBackend( + `[DEBUG-COMPLETE] Chunks: received:${sequencedCount}/${expectedChunks}, isSequenceComplete:${isSequencedComplete}` + ); + postLogToBackend( + `[DEBUG-COMPLETE] Size: current:${currentTotalSize}, expected:${expectedSize}, sizeComplete:${sizeComplete}, diff:${ + expectedSize - currentTotalSize + }` + ); + postLogToBackend( + `[DEBUG-COMPLETE] LastChunks: ${lastFewChunks.join(", ")}` + ); + postLogToBackend( + `[DEBUG-COMPLETE] IsDataComplete: ${isDataComplete}, isFinalized: ${reception.isFinalized}` + ); + + if (reception.sequencedWriter) { + const writerStatus = reception.sequencedWriter.getBufferStatus(); + postLogToBackend( + `[DEBUG-COMPLETE] SequencedWriter: nextIndex:${writerStatus.nextIndex}, totalWritten:${writerStatus.totalWritten}, queueSize:${writerStatus.queueSize}` + ); + } + } + // Prevent duplicate finalize if (reception.isFinalized) { return; @@ -862,6 +989,12 @@ class FileReceiver { if (isDataComplete) { reception.isFinalized = true; + if (developmentEnv === "development") { + postLogToBackend( + `[DEBUG-COMPLETE] ✅ Starting finalization - isDataComplete:${isDataComplete}` + ); + } + try { await this.finalizeFileReceive(); @@ -870,7 +1003,7 @@ class FileReceiver { } this.activeFileReception = null; } catch (error) { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend(`[DEBUG] ❌ Auto-finalize ERROR: ${error}`); } if (reception.completionNotifier) { @@ -958,10 +1091,14 @@ class FileReceiver { startChunkIndex ); - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] 📢 SEQUENCED_WRITER created - startIndex: ${startChunkIndex}, offset: ${offset}` ); + // 🔍 调试续传接收期望 + postLogToBackend( + `[DEBUG-RESUME] 🎯 SequencedWriter expects - startIndex:${startChunkIndex}, offset:${offset}, calculatedFrom:${offset}/65536` + ); } } catch (err) { this.fireError("Failed to create file on disk", { @@ -994,28 +1131,59 @@ class FileReceiver { private async finalizeLargeFileReceive(): Promise { const reception = this.activeFileReception; - if (!reception?.writeStream || !reception.fileHandle) return; + if (!reception?.writeStream || !reception.fileHandle) { + if (developmentEnv === "development") { + postLogToBackend( + `[DEBUG-FINALIZE] ❌ Cannot finalize - missing writeStream:${!!reception?.writeStream} or fileHandle:${!!reception?.fileHandle}` + ); + } + return; + } try { + if (developmentEnv === "development") { + postLogToBackend( + `[DEBUG-FINALIZE] 🚀 Starting finalization for ${reception.meta.name}` + ); + } + // 🚀 First close the strict sequential writing manager (flush all buffers) if (reception.sequencedWriter) { + if (developmentEnv === "development") { + postLogToBackend(`[DEBUG-FINALIZE] Closing SequencedWriter...`); + } await reception.sequencedWriter.close(); const status = reception.sequencedWriter.getBufferStatus(); - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( - `[DEBUG] 💾 SEQUENCED_WRITER closed - totalWritten: ${status.totalWritten}, finalQueue: ${status.queueSize}` + `[DEBUG-FINALIZE] 💾 SEQUENCED_WRITER closed - totalWritten: ${status.totalWritten}, finalQueue: ${status.queueSize}` ); } reception.sequencedWriter = null; } // Then close the file stream + if (developmentEnv === "development") { + postLogToBackend( + `[DEBUG-FINALIZE] About to close writeStream for ${reception.meta.name}` + ); + } await reception.writeStream.close(); + if (developmentEnv === "development") { + postLogToBackend(`[DEBUG-FINALIZE] ✅ WriteStream closed successfully`); + } - if (developmentEnv === "true") { - postLogToBackend(`[DEBUG] ✅ LARGE_FILE finalized successfully`); + if (developmentEnv === "development") { + postLogToBackend( + `[DEBUG-FINALIZE] ✅ LARGE_FILE finalized successfully - ${reception.meta.name}` + ); } } catch (error) { + if (developmentEnv === "development") { + postLogToBackend( + `[DEBUG-FINALIZE] ❌ Error during finalization: ${error}` + ); + } this.fireError("Error finalizing large file", { error }); } } @@ -1040,7 +1208,7 @@ class FileReceiver { // Final verification const sizeDifference = reception.meta.size - totalChunkSize; if (sizeDifference !== 0) { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] ❌ SIZE_MISMATCH - missing: ${sizeDifference} bytes` ); @@ -1131,7 +1299,7 @@ class FileReceiver { this.peerId ); - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] 📤 Sent folderReceiveComplete - folderName: ${folderName}, completedFiles: ${completedFileIds.length}, allStoreUpdated: ${allStoreUpdated}, success: ${success}` ); diff --git a/frontend/lib/tracking.ts b/frontend/lib/tracking.ts index 4cc95b8..b87d5a8 100644 --- a/frontend/lib/tracking.ts +++ b/frontend/lib/tracking.ts @@ -4,7 +4,7 @@ export const trackReferrer = async () => { // Get URL parameters const urlParams = new URLSearchParams(window.location.search); let ref = urlParams.get("ref"); - if (process.env.NEXT_PUBLIC_development === "false") { + if (process.env.NODE_ENV === "production") { ref = urlParams.get("ref") || "noRef"; // Production environment, count daily active users, record as noRef if there is no ref } diff --git a/frontend/lib/transfer/FileTransferOrchestrator.ts b/frontend/lib/transfer/FileTransferOrchestrator.ts index 5d58dc2..842e4e5 100644 --- a/frontend/lib/transfer/FileTransferOrchestrator.ts +++ b/frontend/lib/transfer/FileTransferOrchestrator.ts @@ -14,7 +14,7 @@ import { StreamingFileReader } from "./StreamingFileReader"; import { TransferConfig } from "./TransferConfig"; import WebRTC_Initiator from "../webrtc_Initiator"; import { postLogToBackend } from "@/app/config/api"; -const developmentEnv = process.env.NEXT_PUBLIC_development!; +const developmentEnv = process.env.NODE_ENV; /** * 🚀 File transfer orchestrator * Integrates all components to provide unified file transfer services @@ -211,7 +211,7 @@ export class FileTransferOrchestrator implements MessageHandlerDelegate { peerState.readOffset || 0 ); - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] 🚀 Starting transfer - file: ${file.name}, size: ${( file.size / @@ -301,20 +301,32 @@ export class FileTransferOrchestrator implements MessageHandlerDelegate { } } - if (developmentEnv === "true") { + 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] ✅ Transfer complete - file: ${file.name}, time: ${( + `[DEBUG-CHUNKS] ✅ Transfer complete - file: ${file.name}, time: ${( totalTime / 1000 - ).toFixed(1)}s, speed: ${avgSpeedMBps.toFixed( - 1 - )}MB/s, chunks: ${networkChunkIndex}` + ).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 === "true") { + if (developmentEnv === "development") { postLogToBackend(`[DEBUG] ❌ Transfer error: ${errorMessage}`); } this.fireError(errorMessage, { @@ -428,7 +440,7 @@ export class FileTransferOrchestrator implements MessageHandlerDelegate { public handlePeerReconnection(peerId: string): void { // Clear all transfer states for this peer this.stateManager.clearPeerState(peerId); - if (developmentEnv === "true") + if (developmentEnv === "development") this.log( "log", `Successfully reset transfer state for reconnected peer ${peerId}` @@ -443,7 +455,7 @@ export class FileTransferOrchestrator implements MessageHandlerDelegate { this.networkTransmitter.cleanup(); this.progressTracker.cleanup(); this.messageHandler.cleanup(); - if (developmentEnv === "true") + if (developmentEnv === "development") this.log("log", "FileTransferOrchestrator cleaned up"); } } diff --git a/frontend/lib/transfer/MessageHandler.ts b/frontend/lib/transfer/MessageHandler.ts index 61e8d57..4481c7a 100644 --- a/frontend/lib/transfer/MessageHandler.ts +++ b/frontend/lib/transfer/MessageHandler.ts @@ -6,7 +6,7 @@ import { } from "@/types/webrtc"; import { StateManager } from "./StateManager"; import { postLogToBackend } from "@/app/config/api"; -const developmentEnv = process.env.NEXT_PUBLIC_development!; +const developmentEnv = process.env.NODE_ENV; /** * 🚀 Message handling interface - Communicate with main orchestrator */ @@ -120,7 +120,7 @@ export class MessageHandler { message: FolderReceiveComplete, peerId: string ): void { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] 📥 Folder complete - folderName: ${message.folderName}, files: ${message.completedFileIds.length}` ); @@ -172,7 +172,7 @@ export class MessageHandler { * 🧹 Clean up resources */ public cleanup(): void { - if (developmentEnv === "true") + if (developmentEnv === "development") postLogToBackend("[DEBUG] 🧹 MessageHandler cleaned up"); } } diff --git a/frontend/lib/transfer/NetworkTransmitter.ts b/frontend/lib/transfer/NetworkTransmitter.ts index 7b0da2c..fa897f2 100644 --- a/frontend/lib/transfer/NetworkTransmitter.ts +++ b/frontend/lib/transfer/NetworkTransmitter.ts @@ -2,7 +2,7 @@ import { EmbeddedChunkMeta } from "@/types/webrtc"; import { StateManager } from "./StateManager"; import WebRTC_Initiator from "../webrtc_Initiator"; import { postLogToBackend } from "@/app/config/api"; -const developmentEnv = process.env.NEXT_PUBLIC_development!; +const developmentEnv = process.env.NODE_ENV; /** * 🚀 Network transmitter - Simplified version * Uses WebRTC native bufferedAmountLowThreshold for backpressure control @@ -34,7 +34,7 @@ export class NetworkTransmitter { // Key node logs (development environment only) if ( - developmentEnv === "true" && + developmentEnv === "development" && (metadata.chunkIndex % 100 === 0 || metadata.isLastChunk) ) { postLogToBackend( @@ -48,7 +48,7 @@ export class NetworkTransmitter { return true; } catch (error) { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] ❌ CHUNK #${metadata.chunkIndex} send failed: ${error}` ); @@ -106,7 +106,7 @@ export class NetworkTransmitter { if (!sendResult) { const errorMessage = `sendData failed`; - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend(`[DEBUG] ❌ ${errorMessage}`); } throw new Error(errorMessage); @@ -148,7 +148,7 @@ export class NetworkTransmitter { }); // Only output backpressure logs in development environment - if (developmentEnv === "true") { + if (developmentEnv === "development") { const waitTime = performance.now() - startTime; postLogToBackend( `[DEBUG] 🚀 BACKPRESSURE - wait: ${waitTime.toFixed( @@ -182,7 +182,7 @@ export class NetworkTransmitter { } } catch (error) { const errorMessage = `sendWithBackpressure failed: ${error}`; - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend(`[DEBUG] ❌ ${errorMessage}`); } throw new Error(errorMessage); @@ -239,7 +239,7 @@ export class NetworkTransmitter { * 🧹 Clean up resources */ public cleanup(): void { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend("[DEBUG] 🧹 NetworkTransmitter cleaned up"); } } diff --git a/frontend/lib/transfer/ProgressTracker.ts b/frontend/lib/transfer/ProgressTracker.ts index 18d0d06..f28b6c2 100644 --- a/frontend/lib/transfer/ProgressTracker.ts +++ b/frontend/lib/transfer/ProgressTracker.ts @@ -1,7 +1,7 @@ import { SpeedCalculator } from "@/lib/speedCalculator"; import { StateManager } from "./StateManager"; import { postLogToBackend } from "@/app/config/api"; -const developmentEnv = process.env.NEXT_PUBLIC_development!; +const developmentEnv = process.env.NODE_ENV; /** * 🚀 Progress callback type definition */ @@ -225,7 +225,7 @@ export class ProgressTracker { */ cleanup(): void { // SpeedCalculator internally automatically cleans up expired data - if (developmentEnv === "true") + if (developmentEnv === "development") postLogToBackend("[DEBUG] 🧹 ProgressTracker cleaned up"); } } diff --git a/frontend/lib/transfer/StreamingFileReader.ts b/frontend/lib/transfer/StreamingFileReader.ts index 087c536..3a22e71 100644 --- a/frontend/lib/transfer/StreamingFileReader.ts +++ b/frontend/lib/transfer/StreamingFileReader.ts @@ -1,7 +1,7 @@ import { CustomFile } from "@/types/webrtc"; import { TransferConfig } from "./TransferConfig"; import { postLogToBackend } from "@/app/config/api"; -const developmentEnv = process.env.NEXT_PUBLIC_development!; +const developmentEnv = process.env.NODE_ENV; /** * 🚀 Network chunk interface */ @@ -46,9 +46,11 @@ export class StreamingFileReader { this.file = file; this.totalFileSize = file.size; this.totalFileOffset = startOffset; + // 🔧 修复:续传时currentBatchStartOffset应该从startOffset开始 + this.currentBatchStartOffset = startOffset; this.fileReader = new FileReader(); - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `[DEBUG] 📖 StreamingFileReader created - file: ${file.name}, size: ${( this.totalFileSize / @@ -56,6 +58,13 @@ export class StreamingFileReader { 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}` + ); } } @@ -89,6 +98,15 @@ export class StreamingFileReader { // 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, @@ -160,11 +178,24 @@ export class StreamingFileReader { this.currentBatch = await this.readFileSlice(fileSlice); const readTime = performance.now() - readStartTime; - this.currentBatchStartOffset = this.totalFileOffset; - this.currentChunkIndexInBatch = 0; + 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 === "true") { + if (developmentEnv === "development") { const totalTime = performance.now() - startTime; const speedMBps = batchSize / 1024 / 1024 / (totalTime / 1000); postLogToBackend( @@ -174,9 +205,15 @@ export class StreamingFileReader { 1 )}MB/s` ); + // 🔍 调试batch内索引设置 + postLogToBackend( + `[DEBUG-RESUME] 📖 BATCH loaded - batchStartOffset:${batchStartOffset}, currentChunkIndexInBatch:${ + this.currentChunkIndexInBatch + }, isResume:${batchStartOffset % this.BATCH_SIZE !== 0}` + ); } } catch (error) { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend(`[DEBUG] ❌ BATCH_READ failed: ${error}`); } throw new Error(`Failed to load file batch: ${error}`); @@ -247,7 +284,19 @@ export class StreamingFileReader { this.currentBatchStartOffset / this.BATCH_SIZE ); const chunksInPreviousBatches = batchesBefore * this.CHUNKS_PER_BATCH; - return chunksInPreviousBatches + this.currentChunkIndexInBatch; + 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; } /** @@ -324,10 +373,14 @@ export class StreamingFileReader { this.isFinished = false; this.isReading = false; this.currentBatch = null; - this.currentBatchStartOffset = 0; - this.currentChunkIndexInBatch = 0; - if (developmentEnv === "true") { - postLogToBackend(`[DEBUG] 🔄 StreamingFileReader reset`); + // 🔧 修复:reset时也要正确设置currentBatchStartOffset + this.currentBatchStartOffset = startOffset; + this.currentChunkIndexInBatch = 0; // 重置为0,loadNextBatch会重新计算 + + if (developmentEnv === "development") { + postLogToBackend( + `[DEBUG] 🔄 StreamingFileReader reset - startOffset:${startOffset}` + ); } } diff --git a/frontend/lib/webrtc_Initiator.ts b/frontend/lib/webrtc_Initiator.ts index 05ab205..9cdd619 100644 --- a/frontend/lib/webrtc_Initiator.ts +++ b/frontend/lib/webrtc_Initiator.ts @@ -1,7 +1,7 @@ // Initiator flow: Join room; receive 'ready' event (this event is triggered by the socket server after a new recipient enters) -> createPeerConnection + createDataChannel -> createAndSendOffer import BaseWebRTC, { WebRTCConfig } from "./webrtc_base"; import { postLogToBackend } from "@/app/config/api"; -const developmentEnv = process.env.NEXT_PUBLIC_development!; // Development environment +const developmentEnv = process.env.NODE_ENV; // Development environment export default class WebRTC_Initiator extends BaseWebRTC { constructor(config: WebRTCConfig) { @@ -16,7 +16,7 @@ export default class WebRTC_Initiator extends BaseWebRTC { }); // Add listener for recipient's response this.socket.on("recipient-ready", ({ peerId }) => { - if (developmentEnv === "true") + if (developmentEnv === "development") postLogToBackend( `[Initiator] Received recipient-ready from: ${peerId}` ); @@ -32,7 +32,7 @@ export default class WebRTC_Initiator extends BaseWebRTC { private async handleReady({ peerId }: { peerId: string }): Promise { // Recipient peerId // this.log('log',`Received ready signal from peer ${peerId}`); - if (developmentEnv === "true") + if (developmentEnv === "development") postLogToBackend(`Received ready signal from peer ${peerId}`); await this.createPeerConnection(peerId); await this.createDataChannel(peerId); @@ -48,7 +48,7 @@ export default class WebRTC_Initiator extends BaseWebRTC { from: string; }): Promise { // this.log('log',`Handling answer from peer ${from}`); - if (developmentEnv === "true") + if (developmentEnv === "development") postLogToBackend(`Handling answer from peer ${from}`); const peerConnection = this.peerConnections.get(from); if (!peerConnection) { @@ -96,7 +96,7 @@ export default class WebRTC_Initiator extends BaseWebRTC { // If it is the initiator, create and send an offer to the signaling server to negotiate a connection with the recipient. private async createAndSendOffer(peerId: string): Promise { // this.log('log', `Creating and sending offer to ${peerId}`); - if (developmentEnv === "true") + if (developmentEnv === "development") postLogToBackend(`createAndSendOffer for peerId: ${peerId}`); const peerConnection = this.peerConnections.get(peerId); if (!peerConnection) { diff --git a/frontend/lib/webrtc_base.ts b/frontend/lib/webrtc_base.ts index 967b218..64be02e 100644 --- a/frontend/lib/webrtc_base.ts +++ b/frontend/lib/webrtc_base.ts @@ -2,7 +2,7 @@ import io, { Socket, ManagerOptions, SocketOptions } from "socket.io-client"; import { WakeLockManager } from "./wakeLockManager"; import { postLogToBackend } from "@/app/config/api"; -const developmentEnv = process.env.NEXT_PUBLIC_development!; // Development environment +const developmentEnv = process.env.NODE_ENV; // Development environment export class WebRTCError extends Error { constructor(message: string, public context?: Record) { @@ -131,7 +131,7 @@ export default class BaseWebRTC { this.socket.on("disconnect", () => { this.isInRoom = false; this.isSocketDisconnected = true; - if (developmentEnv === "true") + if (developmentEnv === "development") postLogToBackend( `${this.peerId} disconnect on socket,isInitiator:${this.isInitiator},isInRoom:${this.isInRoom}` ); @@ -153,7 +153,7 @@ export default class BaseWebRTC { if (this.isSocketDisconnected && this.isPeerDisconnected && this.roomId) { // Start reconnection only after both socket and P2P connections are disconnected this.reconnectionInProgress = true; - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend( `Starting reconnection, socket and peer both disconnected. isInitiator:${this.isInitiator}` ); @@ -314,7 +314,7 @@ export default class BaseWebRTC { disconnected: async () => { await this.cleanupExistingConnection(peerId); this.isPeerDisconnected = true; - if (developmentEnv === "true") + if (developmentEnv === "development") postLogToBackend(`p2p disconnected, isInitiator:${this.isInitiator}`); // Attempt to reconnect this.attemptReconnection(); @@ -402,7 +402,7 @@ export default class BaseWebRTC { }; dataChannel.onclose = () => { - if (developmentEnv === "true") { + if (developmentEnv === "development") { postLogToBackend(`DataChannel closed for peer: ${peerId}`); } this.log("log", `Data channel with ${peerId} closed.`); @@ -440,7 +440,7 @@ export default class BaseWebRTC { roomId: this.roomId, }); } - if (developmentEnv === "true") + if (developmentEnv === "development") postLogToBackend( `peerId:${this.socket.id} Successfully joined room: ${response.roomId},isInitiator:${this.isInitiator},isInRoom:${this.isInRoom}` ); @@ -448,7 +448,7 @@ export default class BaseWebRTC { } else { this.isInRoom = false; this.roomId = null; - if (developmentEnv === "true") + if (developmentEnv === "development") postLogToBackend(`Failed to join room,message:${response.message}`); this.fireError("Failed to join room", { message: response.message }); reject(new Error(response.message)); @@ -499,10 +499,10 @@ export default class BaseWebRTC { ? data.byteLength : 0; - if (developmentEnv === "true") - postLogToBackend( - `sendToPeer - type: ${dataType}, size: ${dataSize}, bufferedAmount: ${dataChannel.bufferedAmount}` - ); + // if (developmentEnv === "development") + // postLogToBackend( + // `sendToPeer - type: ${dataType}, size: ${dataSize}, bufferedAmount: ${dataChannel.bufferedAmount}` + // ); dataChannel.send(data); return true; From b5404cea72d534c9a816d03c7ebb0a96edfe577f Mon Sep 17 00:00:00 2001 From: david_bai Date: Sun, 14 Sep 2025 08:36:20 +0800 Subject: [PATCH 03/10] chore:Split the fileReceiver.ts --- frontend/lib/fileReceiver.ts | 1485 ++--------------- frontend/lib/receive/ChunkProcessor.ts | 343 ++++ frontend/lib/receive/FileAssembler.ts | 280 ++++ .../lib/receive/FileReceiveOrchestrator.ts | 666 ++++++++ frontend/lib/receive/MessageProcessor.ts | 302 ++++ frontend/lib/receive/ProgressReporter.ts | 309 ++++ frontend/lib/receive/ReceptionConfig.ts | 74 + frontend/lib/receive/ReceptionStateManager.ts | 358 ++++ frontend/lib/receive/StreamingFileWriter.ts | 398 +++++ frontend/lib/receive/index.ts | 45 + 10 files changed, 2916 insertions(+), 1344 deletions(-) create mode 100644 frontend/lib/receive/ChunkProcessor.ts create mode 100644 frontend/lib/receive/FileAssembler.ts create mode 100644 frontend/lib/receive/FileReceiveOrchestrator.ts create mode 100644 frontend/lib/receive/MessageProcessor.ts create mode 100644 frontend/lib/receive/ProgressReporter.ts create mode 100644 frontend/lib/receive/ReceptionConfig.ts create mode 100644 frontend/lib/receive/ReceptionStateManager.ts create mode 100644 frontend/lib/receive/StreamingFileWriter.ts create mode 100644 frontend/lib/receive/index.ts diff --git a/frontend/lib/fileReceiver.ts b/frontend/lib/fileReceiver.ts index 51992ce..49c28e6 100644 --- a/frontend/lib/fileReceiver.ts +++ b/frontend/lib/fileReceiver.ts @@ -1,321 +1,138 @@ -// 🚀 New Process - Receiver-Dominated File Transfer: -// 1. Receive file metadata (fileMetadata) -// 2. User clicks download, send file request (fileRequest) -// 3. Receive all data chunks, automatically detect integrity -// 4. After completing Store synchronization, proactively send completion confirmation (fileReceiveComplete/folderReceiveComplete) -// Folder Transfer: Repeat single file process, finally send folder completion confirmation -import { SpeedCalculator } from "@/lib/speedCalculator"; +// 🚀 Modernized FileReceiver using modular architecture +// This file now serves as a compatibility layer for the new modular receive system + import WebRTC_Recipient from "./webrtc_Recipient"; -import { - CustomFile, - fileMetadata, - WebRTCMessage, - FolderProgress, - CurrentString, - StringMetadata, - StringChunk, - FileHandlers, - FileMeta, - FileRequest, - FileReceiveComplete, - FolderReceiveComplete, - EmbeddedChunkMeta, -} from "@/types/webrtc"; -import { postLogToBackend } from "@/app/config/api"; -const developmentEnv = process.env.NODE_ENV; +import { CustomFile, fileMetadata } from "@/types/webrtc"; +import { createFileReceiveService, FileReceiveOrchestrator } from "./receive"; + /** - * 🚀 Strict Sequential Buffering Writer - Optimizes large file disk I/O performance + * 🚀 FileReceiver - Compatibility wrapper for the new modular architecture + * + * This class maintains backward compatibility while using the new modular receive system. + * All heavy lifting is now done by the FileReceiveOrchestrator and its specialized modules. */ -class SequencedDiskWriter { - private writeQueue = new Map(); - private nextWriteIndex = 0; - private readonly maxBufferSize = 100; // Buffer up to 100 chunks (approximately 6.4MB) - private readonly stream: FileSystemWritableFileStream; - private totalWritten = 0; - - constructor(stream: FileSystemWritableFileStream, startIndex: number = 0) { - this.stream = stream; - this.nextWriteIndex = startIndex; - } - - /**\n * Write a chunk, automatically managing order and buffering\n */ - async writeChunk(chunkIndex: number, chunk: ArrayBuffer): Promise { - // 🔍 调试writeChunk调用 - if ( - developmentEnv === "development" && - (chunkIndex <= 5 || chunkIndex === this.nextWriteIndex) - ) { - postLogToBackend( - `[DEBUG-RESUME] 🎯 WriteChunk called - received:${chunkIndex}, expected:${ - this.nextWriteIndex - }, match:${chunkIndex === this.nextWriteIndex}` - ); - } - - // 1. If it is the expected next chunk, write immediately - if (chunkIndex === this.nextWriteIndex) { - await this.flushSequentialChunks(chunk); - return; - } - - // 2. If it's a future chunk, buffer it - if (chunkIndex > this.nextWriteIndex) { - if (this.writeQueue.size < this.maxBufferSize) { - this.writeQueue.set(chunkIndex, chunk); - // if (developmentEnv === "development") { - // postLogToBackend( - // `[DEBUG] 📦 BUFFERED chunk #${chunkIndex} (waiting for #${this.nextWriteIndex}), queue: ${this.writeQueue.size}/${this.maxBufferSize}` - // ); - // } - } else { - // Buffer full, forcing processing of the earliest chunk to free up space - await this.forceFlushOldest(); - this.writeQueue.set(chunkIndex, chunk); - // if (developmentEnv === "development") { - // postLogToBackend( - // `[DEBUG] ⚠️ BUFFER_FULL, forced flush and buffered chunk #${chunkIndex}` - // ); - // } - } - return; - } - - // 3. If the chunk is expired, log a warning but ignore (already written) - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG] ⚠️ DUPLICATE chunk #${chunkIndex} ignored (already written #${this.nextWriteIndex})` - ); - } - } - - /** - * Write current chunk and attempt to sequentially write subsequent chunks - */ - private async flushSequentialChunks(firstChunk: ArrayBuffer): Promise { - let flushCount = 0; // 声明移到外部 - - try { - // Write current chunk - await this.stream.write(firstChunk); - this.totalWritten += firstChunk.byteLength; - - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG] ✓ DISK_WRITE chunk #${this.nextWriteIndex} - ${firstChunk.byteLength} bytes, total: ${this.totalWritten}` - ); - } - - this.nextWriteIndex++; - - // Try to sequentially write chunks from buffer - while (this.writeQueue.has(this.nextWriteIndex)) { - const chunk = this.writeQueue.get(this.nextWriteIndex)!; - await this.stream.write(chunk); - this.totalWritten += chunk.byteLength; - this.writeQueue.delete(this.nextWriteIndex); - - flushCount++; - this.nextWriteIndex++; - } - } catch (error) { - // 🔒 防御性处理:如果流已关闭,静默忽略 - const errorMessage = - error instanceof Error ? error.message : String(error); - if ( - errorMessage.includes("closing writable stream") || - errorMessage.includes("stream is closed") - ) { - console.log( - `[SequencedDiskWriter] Stream closed during write - ignoring remaining chunks` - ); - return; - } - // 重新抛出其他类型的错误 - throw error; - } - - if (flushCount > 0) { - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG] 🔥 SEQUENTIAL_FLUSH ${flushCount} chunks, now at #${this.nextWriteIndex}, queue: ${this.writeQueue.size}` - ); - } - } - } - - /** - * Get the next expected write index - */ - get expectedIndex(): number { - return this.nextWriteIndex; - } - - /** - * Force refresh the earliest chunk to release buffer space - */ - private async forceFlushOldest(): Promise { - try { - if (this.writeQueue.size === 0) return; - - const oldestIndex = Math.min(...Array.from(this.writeQueue.keys())); - const chunk = this.writeQueue.get(oldestIndex)!; - - // Warning: Unordered write - // if (developmentEnv === "development") { - // postLogToBackend( - // `[DEBUG] ⚠️ FORCE_FLUSH out-of-order chunk #${oldestIndex} (expected #${this.nextWriteIndex})` - // ); - // } - - // Use seek to write at the correct position (fallback handling) - const fileOffset = oldestIndex * 65536; // Assume each chunk is 64KB - await this.stream.seek(fileOffset); - await this.stream.write(chunk); - this.writeQueue.delete(oldestIndex); - - // Return to current position - const currentOffset = this.nextWriteIndex * 65536; - await this.stream.seek(currentOffset); - } catch (error) { - // 🔒 防御性处理:如果流已关闭,静默忽略 - const errorMessage = - error instanceof Error ? error.message : String(error); - if ( - errorMessage.includes("closing writable stream") || - errorMessage.includes("stream is closed") - ) { - console.log( - `[SequencedDiskWriter] Stream closed during write - ignoring remaining chunks` - ); - return; - } - // 重新抛出其他类型的错误 - throw error; - } - } - - /** - * Get buffer status - */ - getBufferStatus(): { - queueSize: number; - nextIndex: number; - totalWritten: number; - } { - return { - queueSize: this.writeQueue.size, - nextIndex: this.nextWriteIndex, - totalWritten: this.totalWritten, - }; - } - - /** - * Close and clean up resources - */ - async close(): Promise { - try { - // Try to flush all remaining chunks - const remainingIndexes = Array.from(this.writeQueue.keys()).sort( - (a, b) => a - b - ); - for (const chunkIndex of remainingIndexes) { - const chunk = this.writeQueue.get(chunkIndex)!; - const fileOffset = chunkIndex * 65536; - await this.stream.seek(fileOffset); - await this.stream.write(chunk); - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG] 💾 FINAL_FLUSH chunk #${chunkIndex} at cleanup` - ); - } - } - } catch (error) { - // 🔒 防御性处理:关闭时如果流已不可写,静默处理 - const errorMessage = - error instanceof Error ? error.message : String(error); - if ( - errorMessage.includes("closing writable stream") || - errorMessage.includes("stream is closed") - ) { - console.log( - `[SequencedDiskWriter] Stream closed during final flush - data may be incomplete` - ); - } else { - console.warn( - `[SequencedDiskWriter] Error during final flush:`, - errorMessage - ); - throw error; // 重新抛出其他错误 - } - } - - this.writeQueue.clear(); - } -} - -/**\n * 🚀 New Version: Manage file reception state for serialized embedded packets\n */ -interface ActiveFileReception { - meta: fileMetadata; // If meta is present, it means this file is currently being received; null means no file is being received. - chunks: (ArrayBuffer | null)[]; // Array of data chunks arranged by index - receivedSize: number; - initialOffset: number; // For resuming downloads - fileHandle: FileSystemFileHandle | null; // Object related to writing to disk -- current file. - writeStream: FileSystemWritableFileStream | null; // Object related to writing to disk. - sequencedWriter: SequencedDiskWriter | null; // 🚀 Added: Strict sequential writing manager - completionNotifier: { - resolve: () => void; - reject: (reason?: any) => void; - }; - // 🚀 New Version: Simplified sequential reception management - receivedChunksCount: number; // Actual number of chunks received - expectedChunksCount: number; // Expected number of chunks - chunkSequenceMap: Map; // Track which chunks have been received (for chunk numbering) - isFinalized?: boolean; // Flag to prevent duplicate finalize operations -} - class FileReceiver { - // region Private Properties - private readonly webrtcConnection: WebRTC_Recipient; - private readonly largeFileThreshold: number = 1 * 1024 * 1024 * 1024; // 1 GB, larger files will prompt the user to select a directory for direct disk saving. - private readonly speedCalculator: SpeedCalculator; - private fileHandlers: FileHandlers; + private orchestrator: FileReceiveOrchestrator; - private peerId: string = ""; - private saveDirectory: FileSystemDirectoryHandle | null = null; + // Public properties for backward compatibility + public saveType: Record = {}; - // State Management - private pendingFilesMeta = new Map(); // Stores file metadata, fileId: meta - private folderProgresses: Record = {}; // Folder progress information, fileId: {totalSize: 0, receivedSize: 0, fileIds: []}; - public saveType: Record = {}; // fileId or folderName -> isSavedToDisk + // Callbacks - these are forwarded to the orchestrator + public onFileMetaReceived: ((meta: fileMetadata) => void) | undefined = + undefined; + public onStringReceived: ((str: string) => void) | undefined = undefined; + public onFileReceived: ((file: CustomFile) => Promise) | undefined = + undefined; - // Active transfer state - private activeFileReception: ActiveFileReception | null = null; - private activeStringReception: CurrentString | null = null; - private currentFolderName: string | null = null; // The name of the folder currently being received, or null if not receiving a folder. + constructor(webrtcRecipient: WebRTC_Recipient) { + // Create the orchestrator using the factory function + this.orchestrator = createFileReceiveService(webrtcRecipient); - // Callbacks - public onFileMetaReceived: ((meta: fileMetadata) => void) | null = null; - public onStringReceived: ((str: string) => void) | null = null; - public onFileReceived: ((file: CustomFile) => Promise) | null = null; - private progressCallback: - | ((id: string, progress: number, speed: number) => void) - | null = null; - // endregion + // Set up callback forwarding + this.setupCallbackForwarding(); - constructor(WebRTC_recipient: WebRTC_Recipient) { - this.webrtcConnection = WebRTC_recipient; - this.speedCalculator = new SpeedCalculator(); - - this.fileHandlers = { - string: this.handleReceivedStringChunk.bind(this), - stringMetadata: this.handleStringMetadata.bind(this), - fileMeta: this.handleFileMetadata.bind(this), - }; - - this.setupDataHandler(); + this.log("log", "FileReceiver initialized with modular architecture"); } - // region Logging and Error Handling + /** + * Set up callback forwarding to the orchestrator + */ + private setupCallbackForwarding(): void { + // Forward file metadata callback + this.orchestrator.onFileMetaReceived = (meta: fileMetadata) => { + // Update saveType for backward compatibility + this.saveType = this.orchestrator.getSaveType(); + + if (this.onFileMetaReceived) { + this.onFileMetaReceived(meta); + } + }; + + // Forward string received callback + this.orchestrator.onStringReceived = (str: string) => { + if (this.onStringReceived) { + this.onStringReceived(str); + } + }; + + // Forward file received callback + this.orchestrator.onFileReceived = async (file: CustomFile) => { + if (this.onFileReceived) { + await this.onFileReceived(file); + } + }; + } + + /** + * Set progress callback + */ + public setProgressCallback( + callback: (fileId: string, progress: number, speed: number) => void + ): void { + this.orchestrator.setProgressCallback(callback); + } + + /** + * Set save directory + */ + public setSaveDirectory(directory: FileSystemDirectoryHandle): Promise { + return this.orchestrator.setSaveDirectory(directory); + } + + /** + * Request a single file from the peer + */ + public async requestFile(fileId: string, singleFile = true): Promise { + return this.orchestrator.requestFile(fileId, singleFile); + } + + /** + * Request all files belonging to a folder from the peer + */ + public async requestFolder(folderName: string): Promise { + return this.orchestrator.requestFolder(folderName); + } + + /** + * Graceful shutdown + */ + public gracefulShutdown(reason: string = "CONNECTION_LOST"): void { + this.orchestrator.gracefulShutdown(reason); + + // Update saveType for backward compatibility + this.saveType = {}; + } + + /** + * Force reset all internal states + */ + public forceReset(): void { + this.orchestrator.forceReset(); + + // Update saveType for backward compatibility + this.saveType = {}; + } + + /** + * Get transfer statistics (for debugging and monitoring) + */ + public getTransferStats() { + return this.orchestrator.getTransferStats(); + } + + /** + * Clean up all resources + */ + public cleanup(): void { + this.orchestrator.cleanup(); + this.saveType = {}; + } + + // ===== Private Methods ===== + + /** + * Logging utility + */ private log( level: "log" | "warn" | "error", message: string, @@ -325,1056 +142,36 @@ class FileReceiver { console[level](prefix, message, context || ""); } - private fireError(message: string, context?: Record) { - if (this.webrtcConnection.fireError) { - // @ts-ignore - this.webrtcConnection.fireError(message, { - ...context, - component: "FileReceiver", - }); - } else { - this.log("error", message, context); - } + // ===== Backward Compatibility Getters ===== - if (this.activeFileReception) { - // 🚀 Also clean up SequencedWriter on error - if (this.activeFileReception.sequencedWriter) { - this.activeFileReception.sequencedWriter.close().catch((err) => { - this.log( - "error", - "Error closing sequenced writer during error cleanup", - { err } - ); - }); - } - - this.activeFileReception.completionNotifier.reject(new Error(message)); - this.activeFileReception = null; - } - } - // endregion - - // region Setup and Public API - private setupDataHandler(): void { - this.webrtcConnection.onDataReceived = this.handleReceivedData.bind(this); - } - - public setProgressCallback( - callback: (fileId: string, progress: number, speed: number) => void - ): void { - this.progressCallback = callback; - } - - public setSaveDirectory(directory: FileSystemDirectoryHandle): Promise { - this.saveDirectory = directory; - return Promise.resolve(); + /** + * Get pending files metadata (for backward compatibility) + */ + public getPendingFilesMeta(): Map { + return this.orchestrator.getPendingFilesMeta(); } /** - * Requests a single file from the peer. + * Get folder progresses (for backward compatibility) */ - public async requestFile(fileId: string, singleFile = true): Promise { - if (this.activeFileReception) { - this.log("warn", "Another file reception is already in progress."); - return; - } - - if (singleFile) this.currentFolderName = null; - - const fileInfo = this.pendingFilesMeta.get(fileId); - if (!fileInfo) { - this.fireError("File info not found for the requested fileId", { - fileId, - }); - return; - } - - const shouldSaveToDisk = - !!this.saveDirectory || fileInfo.size >= this.largeFileThreshold; - - // Set saveType at the beginning of the request to prevent race conditions in the UI - this.saveType[fileInfo.fileId] = shouldSaveToDisk; - if (this.currentFolderName) { - this.saveType[this.currentFolderName] = shouldSaveToDisk; - } - - let offset = 0; - if (shouldSaveToDisk && this.saveDirectory) { - try { - const folderHandle = await this.createFolderStructure( - fileInfo.fullName - ); - const fileHandle = await folderHandle.getFileHandle(fileInfo.name, { - create: false, - }); - const file = await fileHandle.getFile(); - offset = file.size; - - if (offset === fileInfo.size) { - this.log("log", "File already fully downloaded.", { fileId }); - // Optionally, trigger a "completed" state in the UI directly - this.progressCallback?.(fileId, 1, 0); - return; // Skip the request - } - this.log("log", `Resuming file from offset: ${offset}`, { fileId }); - } catch (e) { - // File does not exist, starting from scratch - this.log("log", "Partial file not found, starting from scratch.", { - fileId, - }); - offset = 0; - } - } - - const receptionPromise = new Promise((resolve, reject) => { - const expectedChunksCount = Math.ceil((fileInfo.size - offset) / 65536); // Calculate expected chunk count - - // 🔍 调试expectedChunksCount计算 - if (developmentEnv === "development") { - const totalChunks = Math.ceil(fileInfo.size / 65536); - const startChunkIndex = Math.floor(offset / 65536); - const calculatedExpected = totalChunks - startChunkIndex; - - postLogToBackend(`[DEBUG-CHUNKS] File: ${fileInfo.name}`); - postLogToBackend( - `[DEBUG-CHUNKS] File size: ${fileInfo.size}, offset: ${offset}` - ); - postLogToBackend( - `[DEBUG-CHUNKS] Total chunks in file: ${totalChunks} (0-${ - totalChunks - 1 - })` - ); - postLogToBackend( - `[DEBUG-CHUNKS] Start chunk index: ${startChunkIndex}` - ); - postLogToBackend( - `[DEBUG-CHUNKS] Expected chunks calculation: (${fileInfo.size} - ${offset}) / 65536 = ${expectedChunksCount}` - ); - postLogToBackend( - `[DEBUG-CHUNKS] Alternative calculation: ${totalChunks} - ${startChunkIndex} = ${calculatedExpected}` - ); - - if (expectedChunksCount !== calculatedExpected) { - postLogToBackend( - `[DEBUG-CHUNKS] ⚠️ MISMATCH: ${expectedChunksCount} vs ${calculatedExpected}` - ); - } - } - - this.activeFileReception = { - meta: fileInfo, - chunks: new Array(expectedChunksCount).fill(null), // 🚀 Initialize as an empty array arranged by index - receivedSize: 0, - initialOffset: offset, - fileHandle: null, - writeStream: null, - sequencedWriter: null, // 🚀 Added: Strict sequential writing manager - completionNotifier: { resolve, reject }, - // 🚀 New Version: Simplified sequential reception management - receivedChunksCount: 0, - expectedChunksCount: expectedChunksCount, - chunkSequenceMap: new Map(), - }; - }); - - if (shouldSaveToDisk) { - await this.createDiskWriteStream(fileInfo, offset); - } - - const request: FileRequest = { type: "fileRequest", fileId, offset }; - if (this.peerId) { - this.webrtcConnection.sendData(JSON.stringify(request), this.peerId); - this.log("log", "Sent fileRequest", { request }); - } else { - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG] ERROR: Cannot send fileRequest - no peerId available!` - ); - } - } - - return receptionPromise; + public getFolderProgresses(): Record { + return this.orchestrator.getFolderProgresses(); } /** - * Requests all files belonging to a folder from the peer. + * Check if there's an active file reception */ - public async requestFolder(folderName: string): Promise { - const folderProgress = this.folderProgresses[folderName]; - if (!folderProgress || folderProgress.fileIds.length === 0) { - this.log("warn", "No files found for the requested folder.", { - folderName, - }); - return; - } - - // Pre-calculate total size of already downloaded parts of the folder - let initialFolderReceivedSize = 0; - if (this.saveDirectory) { - for (const fileId of folderProgress.fileIds) { - const fileInfo = this.pendingFilesMeta.get(fileId); - if (fileInfo) { - try { - const folderHandle = await this.createFolderStructure( - fileInfo.fullName - ); - const fileHandle = await folderHandle.getFileHandle(fileInfo.name, { - create: false, - }); - const file = await fileHandle.getFile(); - initialFolderReceivedSize += file.size; - } catch (e) { - // File doesn't exist, so its size is 0. - } - } - } - } - folderProgress.receivedSize = initialFolderReceivedSize; - this.log( - "log", - `Requesting to receive folder, initial received size: ${initialFolderReceivedSize}`, - { folderName } - ); - - this.currentFolderName = folderName; - for (const fileId of folderProgress.fileIds) { - try { - await this.requestFile(fileId, false); - } catch (error) { - this.fireError( - `Failed to receive file ${fileId} in folder ${folderName}`, - { error } - ); - // Stop receiving other files in the folder on error - break; - } - } - this.currentFolderName = null; - - // 🚀 New Process: Send folder reception completion confirmation - // Collect all successfully completed file IDs - const completedFileIds = folderProgress.fileIds.filter((fileId) => { - // More complex validation logic can be added here, now simply assume all succeeded - return true; - }); - - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG] 📁 All files in folder completed - ${folderName}, files: ${completedFileIds.length}/${folderProgress.fileIds.length}` - ); - } - - // Send folder completion message - this.sendFolderReceiveComplete(folderName, completedFileIds, true); - } - // endregion - - // region WebRTC Data Handlers - - /** - * Convert various binary data formats to ArrayBuffer - * Supports Blob, Uint8Array, and other formats for Firefox - */ - private async convertToArrayBuffer(data: any): Promise { - const originalType = Object.prototype.toString.call(data); - - if (data instanceof ArrayBuffer) { - return data; - } else if (data instanceof Blob) { - try { - const arrayBuffer = await data.arrayBuffer(); - if (data.size !== arrayBuffer.byteLength) { - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG] ⚠️ Blob size mismatch: ${data.size}→${arrayBuffer.byteLength}` - ); - } - } - return arrayBuffer; - } catch (error) { - if (developmentEnv === "development") { - postLogToBackend(`[DEBUG] ❌ Blob conversion failed: ${error}`); - } - return null; - } - } else if (data instanceof Uint8Array || ArrayBuffer.isView(data)) { - try { - const uint8Array = - data instanceof Uint8Array - ? data - : new Uint8Array(data.buffer, data.byteOffset, data.byteLength); - const newArrayBuffer = new ArrayBuffer(uint8Array.length); - new Uint8Array(newArrayBuffer).set(uint8Array); - return newArrayBuffer; - } catch (error) { - if (developmentEnv === "development") { - postLogToBackend(`[DEBUG] ❌ TypedArray conversion failed: ${error}`); - } - return null; - } - } else { - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG] ❌ Unknown data type: ${Object.prototype.toString.call( - data - )}` - ); - } - return null; - } + public hasActiveFileReception(): boolean { + const stats = this.orchestrator.getTransferStats(); + return stats.stateManager.hasActiveFileReception; } /** - * 🚀 Parsing fusion packets - * Format: [4 bytes length] + [JSON metadata] + [actual chunk data] + * Get current peer ID */ - private parseEmbeddedChunkPacket(arrayBuffer: ArrayBuffer): { - chunkMeta: EmbeddedChunkMeta; - chunkData: ArrayBuffer; - } | null { - try { - // 1. Check minimum packet length - if (arrayBuffer.byteLength < 4) { - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG] ❌ Invalid embedded packet - too small: ${arrayBuffer.byteLength}` - ); - } - return null; - } - - // 2. Read metadata length (4 bytes) - const lengthView = new Uint32Array(arrayBuffer, 0, 1); - const metaLength = lengthView[0]; - - // 3. Verify packet integrity - const expectedTotalLength = 4 + metaLength; - if (arrayBuffer.byteLength < expectedTotalLength) { - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG] ❌ Incomplete embedded packet - expected: ${expectedTotalLength}, got: ${arrayBuffer.byteLength}` - ); - } - return null; - } - - // 4. Extract metadata section - const metaBytes = new Uint8Array(arrayBuffer, 4, metaLength); - const metaJson = new TextDecoder().decode(metaBytes); - const chunkMeta: EmbeddedChunkMeta = JSON.parse(metaJson); - - // 5. Extract actual chunk data section - const chunkDataStart = 4 + metaLength; - const chunkData = arrayBuffer.slice(chunkDataStart); - - // 6. Verify chunk data size - if (chunkData.byteLength !== chunkMeta.chunkSize) { - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG] ⚠️ Chunk size mismatch - meta: ${chunkMeta.chunkSize}, actual: ${chunkData.byteLength}` - ); - } - } - - return { chunkMeta, chunkData }; - } catch (error) { - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG] ❌ Failed to parse embedded packet: ${error}` - ); - } - return null; - } - } - - private async handleReceivedData( - data: string | ArrayBuffer | any, - peerId: string - ): Promise { - this.peerId = peerId; - - if (typeof data === "string") { - try { - const parsedData = JSON.parse(data) as WebRTCMessage; - - const handler = - this.fileHandlers[parsedData.type as keyof FileHandlers]; - if (handler) { - await handler(parsedData as any, peerId); - } else { - console.warn( - `[DEBUG] ⚠️ FileReceiver Handler not found: ${parsedData.type}` - ); - } - } catch (error) { - this.fireError("Error parsing received JSON data", { error }); - } - } else { - // 🚀 New Version: Process embedded packets - Completely solve Firefox out-of-order issue - const arrayBuffer = await this.convertToArrayBuffer(data); - - if (arrayBuffer) { - if (!this.activeFileReception) { - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG] ERROR: Received file chunk but no active file reception!` - ); - } - this.fireError( - "Received a file chunk without an active file reception.", - { peerId } - ); - return; - } - - // 🚀 Unified processing: All data is processed as embedded packets - await this.handleEmbeddedChunkPacket(arrayBuffer); - } else { - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG] ERROR: Failed to convert binary data to ArrayBuffer` - ); - } - this.fireError("Received unsupported binary data format", { - dataType: Object.prototype.toString.call(data), - peerId, - }); - } - } - } - - private handleFileMetadata(metadata: fileMetadata): void { - if (this.pendingFilesMeta.has(metadata.fileId)) { - return; // Ignore if already received. - } - - this.pendingFilesMeta.set(metadata.fileId, metadata); - - if (this.onFileMetaReceived) { - this.onFileMetaReceived(metadata); - } else { - console.error( - `[DEBUG] ❌ FileReceiver onFileMetaReceived callback does not exist!` - ); - } - // Record the file size for folder progress calculation. - if (metadata.folderName) { - const folderId = metadata.folderName; - if (!(folderId in this.folderProgresses)) { - this.folderProgresses[folderId] = { - totalSize: 0, - receivedSize: 0, - fileIds: [], - }; - } - const folderProgress = this.folderProgresses[folderId]; - if (!folderProgress.fileIds.includes(metadata.fileId)) { - // Prevent duplicate calculation - folderProgress.totalSize += metadata.size; - folderProgress.fileIds.push(metadata.fileId); - } - } - } - - private handleStringMetadata(metadata: StringMetadata): void { - this.activeStringReception = { - length: metadata.length, - chunks: [], - receivedChunks: 0, - }; - } - - private handleReceivedStringChunk(data: StringChunk): void { - if (!this.activeStringReception) return; - - this.activeStringReception.chunks[data.index] = data.chunk; - this.activeStringReception.receivedChunks++; - - if (this.activeStringReception.receivedChunks === data.total) { - const fullString = this.activeStringReception.chunks.join(""); - this.onStringReceived?.(fullString); - this.activeStringReception = null; - } - } - - // region File and Folder Processing - - /** - * 🚀 New Version: Process embedded packets - */ - private async handleEmbeddedChunkPacket( - arrayBuffer: ArrayBuffer - ): Promise { - const parsed = this.parseEmbeddedChunkPacket(arrayBuffer); - if (!parsed) { - this.fireError("Failed to parse embedded chunk packet"); - return; - } - - const { chunkMeta, chunkData } = parsed; - - // 🔒 防御性检查:确保文件接收还在活跃状态 - const reception = this.activeFileReception; - if (!reception) { - console.log( - `[FileReceiver] Ignoring chunk ${chunkMeta.chunkIndex} - file reception already closed` - ); - return; - } - - // Verify fileId match - if (chunkMeta.fileId !== reception.meta.fileId) { - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG] ⚠️ FileId mismatch - expected: ${reception.meta.fileId}, got: ${chunkMeta.fileId}` - ); - } - return; - } - - // 🔧 修复:续传时不要调整expectedChunksCount - // chunkMeta.totalChunks是文件总chunk数,但续传时我们只接收部分chunks - if (chunkMeta.totalChunks !== reception.expectedChunksCount) { - if (developmentEnv === "development") { - const startChunkIndex = Math.floor(reception.initialOffset / 65536); - const calculatedExpected = chunkMeta.totalChunks - startChunkIndex; - postLogToBackend( - `[DEBUG-CHUNKS] Chunk count info - fileTotal: ${chunkMeta.totalChunks}, currentExpected: ${reception.expectedChunksCount}, calculatedExpected: ${calculatedExpected}, startChunk: ${startChunkIndex}` - ); - - // 🚫 不再调整expectedChunksCount,保持续传时的正确数量 - // reception.expectedChunksCount = chunkMeta.totalChunks; // 这行导致了问题! - - if (reception.expectedChunksCount !== calculatedExpected) { - postLogToBackend( - `[DEBUG-CHUNKS] ⚠️ Expected chunks mismatch, should be ${calculatedExpected}` - ); - } - } - } - - // Store chunk by index - 🔧 修复:将绝对索引映射到相对索引 - const absoluteChunkIndex = chunkMeta.chunkIndex; // 发送端的绝对索引(如967-3650) - const startChunkIndex = Math.floor(reception.initialOffset / 65536); // 续传起始索引(如967) - const relativeChunkIndex = absoluteChunkIndex - startChunkIndex; // 在chunks数组中的相对索引(如0-2683) - - if (developmentEnv === "development" && absoluteChunkIndex <= 970) { - postLogToBackend( - `[DEBUG-CHUNKS] Index mapping - absolute:${absoluteChunkIndex}, start:${startChunkIndex}, relative:${relativeChunkIndex}, arraySize:${reception.chunks.length}` - ); - } - - if ( - relativeChunkIndex >= 0 && - relativeChunkIndex < reception.chunks.length - ) { - reception.chunks[relativeChunkIndex] = chunkData; - reception.chunkSequenceMap.set(absoluteChunkIndex, true); // 序列映射仍使用绝对索引 - reception.receivedChunksCount++; - - // Update progress - this.updateProgress(chunkData.byteLength); - - if (reception.sequencedWriter) { - // 🔍 调试chunk接收匹配 (前5个和后5个chunks) - const lastFewChunks = - relativeChunkIndex >= reception.expectedChunksCount - 5; - if ( - developmentEnv === "development" && - (absoluteChunkIndex <= 970 || lastFewChunks) - ) { - postLogToBackend( - `[DEBUG-CHUNKS] 📦 Chunk #${absoluteChunkIndex} received - relative:${relativeChunkIndex}, size:${chunkData.byteLength}, writerExpects:${reception.sequencedWriter.expectedIndex}, isLastFew:${lastFewChunks}` - ); - } - - // 🚀 Use strict sequential write management - 使用绝对索引 - await reception.sequencedWriter.writeChunk( - absoluteChunkIndex, - chunkData - ); - } - } else { - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG-CHUNKS] ❌ Invalid relative chunk index - absolute:${absoluteChunkIndex}, relative:${relativeChunkIndex}, arraySize:${ - reception.chunks.length - }, expected:0-${reception.chunks.length - 1}` - ); - } - } - - await this.checkAndAutoFinalize(); - } - - /** - * 🚀 Unified auto-complete check - */ - private async checkAndAutoFinalize(): Promise { - if (!this.activeFileReception) return; - - const reception = this.activeFileReception; - const receivedChunks = reception.receivedChunksCount; - const expectedChunks = reception.expectedChunksCount; - - // Calculate current actual total received size - const currentTotalSize = reception.chunks.reduce((sum, chunk) => { - return sum + (chunk instanceof ArrayBuffer ? chunk.byteLength : 0); - }, 0); - // 🔧 修复:续传时应该比较的是剩余文件大小,不是整个文件大小 - const expectedSize = reception.meta.size - reception.initialOffset; - - // 🚀 Unified integrity check: sequential reception mode - let sequencedCount = 0; - for (let i = 0; i < expectedChunks; i++) { - if (reception.chunks[i] instanceof ArrayBuffer) { - sequencedCount++; - } - } - const isSequencedComplete = sequencedCount === expectedChunks; - const sizeComplete = currentTotalSize >= expectedSize; - const isDataComplete = isSequencedComplete && sizeComplete; - - // 🔍 详细调试完成检查 (减少频率,只在关键时刻输出) - if ( - developmentEnv === "development" && - (isDataComplete || - sequencedCount % 500 === 0 || - sequencedCount > expectedChunks - 10) - ) { - // 检查最后几个chunks的状态 (显示相对索引) - const lastChunkIndex = expectedChunks - 1; - const lastFewChunks = []; - const startChunkIndex = Math.floor(reception.initialOffset / 65536); - - for (let i = Math.max(0, lastChunkIndex - 3); i <= lastChunkIndex; i++) { - const chunk = reception.chunks[i]; - const exists = chunk instanceof ArrayBuffer; - const size = exists ? (chunk as ArrayBuffer).byteLength : 0; - const absoluteIndex = startChunkIndex + i; // 对应的绝对索引 - lastFewChunks.push(`rel#${i}(abs#${absoluteIndex}):${exists}(${size})`); - } - - postLogToBackend( - `[DEBUG-COMPLETE] Check completion - file:${reception.meta.name}` - ); - postLogToBackend( - `[DEBUG-COMPLETE] Chunks: received:${sequencedCount}/${expectedChunks}, isSequenceComplete:${isSequencedComplete}` - ); - postLogToBackend( - `[DEBUG-COMPLETE] Size: current:${currentTotalSize}, expected:${expectedSize}, sizeComplete:${sizeComplete}, diff:${ - expectedSize - currentTotalSize - }` - ); - postLogToBackend( - `[DEBUG-COMPLETE] LastChunks: ${lastFewChunks.join(", ")}` - ); - postLogToBackend( - `[DEBUG-COMPLETE] IsDataComplete: ${isDataComplete}, isFinalized: ${reception.isFinalized}` - ); - - if (reception.sequencedWriter) { - const writerStatus = reception.sequencedWriter.getBufferStatus(); - postLogToBackend( - `[DEBUG-COMPLETE] SequencedWriter: nextIndex:${writerStatus.nextIndex}, totalWritten:${writerStatus.totalWritten}, queueSize:${writerStatus.queueSize}` - ); - } - } - - // Prevent duplicate finalize - if (reception.isFinalized) { - return; - } - - if (isDataComplete) { - reception.isFinalized = true; - - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG-COMPLETE] ✅ Starting finalization - isDataComplete:${isDataComplete}` - ); - } - - try { - await this.finalizeFileReceive(); - - if (reception.completionNotifier) { - reception.completionNotifier.resolve(); - } - this.activeFileReception = null; - } catch (error) { - if (developmentEnv === "development") { - postLogToBackend(`[DEBUG] ❌ Auto-finalize ERROR: ${error}`); - } - if (reception.completionNotifier) { - reception.completionNotifier.reject(error); - } - this.activeFileReception = null; - } - } - } - - private async finalizeFileReceive(): Promise { - if (!this.activeFileReception) return; - - if (this.activeFileReception.writeStream) { - await this.finalizeLargeFileReceive(); - } else { - await this.finalizeMemoryFileReceive(); - } - } - - private updateProgress(byteLength: number): void { - if (!this.peerId || !this.activeFileReception) return; - - this.activeFileReception.receivedSize += byteLength; - const reception = this.activeFileReception; - const totalReceived = reception.initialOffset + reception.receivedSize; - - if (this.currentFolderName) { - const folderProgress = this.folderProgresses[this.currentFolderName]; - if (!folderProgress) return; - // This is tricky: folder progress needs to sum up individual file progresses. - // For simplicity, we'll estimate based on total received for the active file. - // A more accurate implementation would track offsets for all files in the folder. - folderProgress.receivedSize += byteLength; // This is an approximation - - this.speedCalculator.updateSendSpeed( - this.peerId, - folderProgress.receivedSize - ); - const speed = this.speedCalculator.getSendSpeed(this.peerId); - const progress = - folderProgress.totalSize > 0 - ? folderProgress.receivedSize / folderProgress.totalSize - : 0; - this.progressCallback?.(this.currentFolderName, progress, speed); - } else { - this.speedCalculator.updateSendSpeed(this.peerId, totalReceived); - const speed = this.speedCalculator.getSendSpeed(this.peerId); - const progress = - reception.meta.size > 0 ? totalReceived / reception.meta.size : 0; - this.progressCallback?.(reception.meta.fileId, progress, speed); - } - } - // endregion - - // region Disk Operations - private async createDiskWriteStream( - meta: FileMeta, - offset: number - ): Promise { - if (!this.saveDirectory || !this.activeFileReception) { - this.log("warn", "Save directory not set, falling back to in-memory."); - return; - } - - try { - const folderHandle = await this.createFolderStructure(meta.fullName); - const fileHandle = await folderHandle.getFileHandle(meta.name, { - create: true, - }); - // Use keepExistingData: true to append - const writeStream = await fileHandle.createWritable({ - keepExistingData: true, - }); - // Seek to the offset to start writing from there - await writeStream.seek(offset); - - this.activeFileReception.fileHandle = fileHandle; - this.activeFileReception.writeStream = writeStream; - - // 🚀 Create a strictly sequential write manager - const startChunkIndex = Math.floor(offset / 65536); // Calculate starting chunk index - this.activeFileReception.sequencedWriter = new SequencedDiskWriter( - writeStream, - startChunkIndex - ); - - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG] 📢 SEQUENCED_WRITER created - startIndex: ${startChunkIndex}, offset: ${offset}` - ); - // 🔍 调试续传接收期望 - postLogToBackend( - `[DEBUG-RESUME] 🎯 SequencedWriter expects - startIndex:${startChunkIndex}, offset:${offset}, calculatedFrom:${offset}/65536` - ); - } - } catch (err) { - this.fireError("Failed to create file on disk", { - err, - fileName: meta.name, - }); - } - } - - private async createFolderStructure( - fullName: string - ): Promise { - if (!this.saveDirectory) { - throw new Error("Save directory not set"); - } - - const parts = fullName.split("/"); - parts.pop(); // Remove filename - - let currentDir = this.saveDirectory; - for (const part of parts) { - if (part) { - currentDir = await currentDir.getDirectoryHandle(part, { - create: true, - }); - } - } - return currentDir; - } - - private async finalizeLargeFileReceive(): Promise { - const reception = this.activeFileReception; - if (!reception?.writeStream || !reception.fileHandle) { - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG-FINALIZE] ❌ Cannot finalize - missing writeStream:${!!reception?.writeStream} or fileHandle:${!!reception?.fileHandle}` - ); - } - return; - } - - try { - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG-FINALIZE] 🚀 Starting finalization for ${reception.meta.name}` - ); - } - - // 🚀 First close the strict sequential writing manager (flush all buffers) - if (reception.sequencedWriter) { - if (developmentEnv === "development") { - postLogToBackend(`[DEBUG-FINALIZE] Closing SequencedWriter...`); - } - await reception.sequencedWriter.close(); - const status = reception.sequencedWriter.getBufferStatus(); - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG-FINALIZE] 💾 SEQUENCED_WRITER closed - totalWritten: ${status.totalWritten}, finalQueue: ${status.queueSize}` - ); - } - reception.sequencedWriter = null; - } - - // Then close the file stream - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG-FINALIZE] About to close writeStream for ${reception.meta.name}` - ); - } - await reception.writeStream.close(); - if (developmentEnv === "development") { - postLogToBackend(`[DEBUG-FINALIZE] ✅ WriteStream closed successfully`); - } - - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG-FINALIZE] ✅ LARGE_FILE finalized successfully - ${reception.meta.name}` - ); - } - } catch (error) { - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG-FINALIZE] ❌ Error during finalization: ${error}` - ); - } - this.fireError("Error finalizing large file", { error }); - } - } - // endregion - - // region In-Memory Operations - private async finalizeMemoryFileReceive(): Promise { - const reception = this.activeFileReception; - if (!reception) return; - - // 🚀 Simplified: Verify sequentially received data - let totalChunkSize = 0; - let validChunks = 0; - - reception.chunks.forEach((chunk, index) => { - if (chunk instanceof ArrayBuffer) { - validChunks++; - totalChunkSize += chunk.byteLength; - } - }); - - // Final verification - const sizeDifference = reception.meta.size - totalChunkSize; - if (sizeDifference !== 0) { - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG] ❌ SIZE_MISMATCH - missing: ${sizeDifference} bytes` - ); - } - } - - // Create file - const fileBlob = new Blob( - reception.chunks.filter( - (chunk) => chunk instanceof ArrayBuffer - ) as ArrayBuffer[], - { - type: reception.meta.fileType, - } - ); - - const file = new File([fileBlob], reception.meta.name, { - type: reception.meta.fileType, - }); - - const customFile = Object.assign(file, { - fullName: reception.meta.fullName, - folderName: this.currentFolderName, - }) as CustomFile; - - let storeUpdated = false; - if (this.onFileReceived) { - await this.onFileReceived(customFile); - await Promise.resolve(); - await new Promise((resolve) => setTimeout(() => resolve(), 0)); - storeUpdated = true; - } - - // Send completion confirmation - this.sendFileReceiveComplete( - reception.meta.fileId, - totalChunkSize, - validChunks, - storeUpdated - ); - } - // region Communication - - /** - * Send file reception completion confirmation - New receiver-dominated process - */ - private sendFileReceiveComplete( - fileId: string, - receivedSize: number, - receivedChunks: number, - storeUpdated: boolean - ): void { - if (!this.peerId) return; - - const completeMessage: FileReceiveComplete = { - type: "fileReceiveComplete", - fileId, - receivedSize, - receivedChunks, - storeUpdated, - }; - - const success = this.webrtcConnection.sendData( - JSON.stringify(completeMessage), - this.peerId - ); - } - - /** - * Send folder reception completion confirmation - */ - private sendFolderReceiveComplete( - folderName: string, - completedFileIds: string[], - allStoreUpdated: boolean - ): void { - if (!this.peerId) return; - - const completeMessage: FolderReceiveComplete = { - type: "folderReceiveComplete", - folderName, - completedFileIds, - allStoreUpdated, - }; - - const success = this.webrtcConnection.sendData( - JSON.stringify(completeMessage), - this.peerId - ); - - if (developmentEnv === "development") { - postLogToBackend( - `[DEBUG] 📤 Sent folderReceiveComplete - folderName: ${folderName}, completedFiles: ${completedFileIds.length}, allStoreUpdated: ${allStoreUpdated}, success: ${success}` - ); - } - } - // endregion - - public gracefulShutdown(reason: string = "CONNECTION_LOST"): void { - this.log("log", `Graceful shutdown initiated: ${reason}`); - - if (this.activeFileReception?.sequencedWriter) { - this.log( - "log", - "Attempting to gracefully close sequenced writer on page unload." - ); - // 🚀 First close the strict sequential writing manager - this.activeFileReception.sequencedWriter.close().catch((err) => { - this.log( - "error", - "Error closing sequenced writer during graceful shutdown", - { - err, - } - ); - }); - } - - if (this.activeFileReception?.writeStream) { - this.log( - "log", - "Attempting to gracefully close write stream on page unload." - ); - // We don't await this, as beforeunload does not wait for promises. - // This is a "best effort" attempt to flush the buffer to disk. - this.activeFileReception.writeStream.close().catch((err) => { - this.log("error", "Error closing stream during graceful shutdown", { - err, - }); - }); - } - - // 🔧 Clean up all internal states to ensure correct file metadata reception upon reconnection - this.pendingFilesMeta.clear(); - this.folderProgresses = {}; - this.saveType = {}; - this.activeFileReception = null; - this.activeStringReception = null; - this.currentFolderName = null; - - this.log("log", "Graceful shutdown completed"); - } - - /** - * Force reset all internal states - used when rejoining rooms - */ - public forceReset(): void { - this.log("log", "Force resetting FileReceiver state"); - - // Close any active streams first - if (this.activeFileReception?.sequencedWriter) { - this.activeFileReception.sequencedWriter.close().catch(console.error); - } - if (this.activeFileReception?.writeStream) { - this.activeFileReception.writeStream.close().catch(console.error); - } - - // Clear all states - this.pendingFilesMeta.clear(); - this.folderProgresses = {}; - this.saveType = {}; - this.activeFileReception = null; - this.activeStringReception = null; - this.currentFolderName = null; - - this.log("log", "FileReceiver state force reset completed"); + public getCurrentPeerId(): string { + const stats = this.orchestrator.getTransferStats(); + return stats.stateManager.currentPeerId; } } diff --git a/frontend/lib/receive/ChunkProcessor.ts b/frontend/lib/receive/ChunkProcessor.ts new file mode 100644 index 0000000..0c6b846 --- /dev/null +++ b/frontend/lib/receive/ChunkProcessor.ts @@ -0,0 +1,343 @@ +import { EmbeddedChunkMeta } from "@/types/webrtc"; +import { ReceptionConfig } from "./ReceptionConfig"; +import { postLogToBackend } from "@/app/config/api"; + +const developmentEnv = process.env.NODE_ENV; + +/** + * 🚀 Chunk processing result interface + */ +export interface ChunkProcessingResult { + chunkMeta: EmbeddedChunkMeta; + chunkData: ArrayBuffer; + absoluteChunkIndex: number; + relativeChunkIndex: number; +} + +/** + * 🚀 Chunk processor + * Handles all data chunk processing, format conversion, and parsing + */ +export class ChunkProcessor { + /** + * Convert various binary data formats to ArrayBuffer + * Supports Blob, Uint8Array, and other formats for Firefox compatibility + */ + async convertToArrayBuffer(data: any): Promise { + const originalType = Object.prototype.toString.call(data); + + if (data instanceof ArrayBuffer) { + return data; + } else if (data instanceof Blob) { + try { + const arrayBuffer = await data.arrayBuffer(); + if (data.size !== arrayBuffer.byteLength) { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG] ⚠️ Blob size mismatch: ${data.size}→${arrayBuffer.byteLength}` + ); + } + } + return arrayBuffer; + } catch (error) { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend(`[DEBUG] ❌ Blob conversion failed: ${error}`); + } + return null; + } + } else if (data instanceof Uint8Array || ArrayBuffer.isView(data)) { + try { + const uint8Array = + data instanceof Uint8Array + ? data + : new Uint8Array(data.buffer, data.byteOffset, data.byteLength); + const newArrayBuffer = new ArrayBuffer(uint8Array.length); + new Uint8Array(newArrayBuffer).set(uint8Array); + return newArrayBuffer; + } catch (error) { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend(`[DEBUG] ❌ TypedArray conversion failed: ${error}`); + } + return null; + } + } else { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG] ❌ Unknown data type: ${Object.prototype.toString.call( + data + )}` + ); + } + return null; + } + } + + /** + * Parse embedded chunk packet + * Format: [4 bytes length] + [JSON metadata] + [actual chunk data] + */ + parseEmbeddedChunkPacket(arrayBuffer: ArrayBuffer): { + chunkMeta: EmbeddedChunkMeta; + chunkData: ArrayBuffer; + } | null { + try { + // 1. Check minimum packet length + if (arrayBuffer.byteLength < ReceptionConfig.VALIDATION_CONFIG.MIN_PACKET_SIZE) { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG] ❌ Invalid embedded packet - too small: ${arrayBuffer.byteLength}` + ); + } + return null; + } + + // 2. Read metadata length (4 bytes) + const lengthView = new Uint32Array(arrayBuffer, 0, 1); + const metaLength = lengthView[0]; + + // 3. Verify packet integrity + const expectedTotalLength = 4 + metaLength; + if (arrayBuffer.byteLength < expectedTotalLength) { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG] ❌ Incomplete embedded packet - expected: ${expectedTotalLength}, got: ${arrayBuffer.byteLength}` + ); + } + return null; + } + + // 4. Extract metadata section + const metaBytes = new Uint8Array(arrayBuffer, 4, metaLength); + const metaJson = new TextDecoder().decode(metaBytes); + const chunkMeta: EmbeddedChunkMeta = JSON.parse(metaJson); + + // 5. Extract actual chunk data section + const chunkDataStart = 4 + metaLength; + const chunkData = arrayBuffer.slice(chunkDataStart); + + // 6. Verify chunk data size + if (chunkData.byteLength !== chunkMeta.chunkSize) { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG] ⚠️ Chunk size mismatch - meta: ${chunkMeta.chunkSize}, actual: ${chunkData.byteLength}` + ); + } + } + + return { chunkMeta, chunkData }; + } catch (error) { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG] ❌ Failed to parse embedded packet: ${error}` + ); + } + return null; + } + } + + /** + * Process received chunk and calculate indices + */ + processReceivedChunk( + chunkMeta: EmbeddedChunkMeta, + chunkData: ArrayBuffer, + initialOffset: number + ): ChunkProcessingResult | null { + // Calculate indices + const absoluteChunkIndex = chunkMeta.chunkIndex; // Sender's absolute index + const startChunkIndex = ReceptionConfig.getChunkIndexFromOffset(initialOffset); // Resume start index + const relativeChunkIndex = absoluteChunkIndex - startChunkIndex; // Relative index in chunks array + + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING && absoluteChunkIndex <= 970) { + postLogToBackend( + `[DEBUG-CHUNKS] Index mapping - absolute:${absoluteChunkIndex}, start:${startChunkIndex}, relative:${relativeChunkIndex}` + ); + } + + return { + chunkMeta, + chunkData, + absoluteChunkIndex, + relativeChunkIndex, + }; + } + + /** + * Validate chunk against expected parameters + */ + validateChunk( + chunkMeta: EmbeddedChunkMeta, + expectedFileId: string, + expectedChunksCount: number, + initialOffset: number + ): { + isValid: boolean; + errors: string[]; + } { + const errors: string[] = []; + + // Verify fileId match + if (chunkMeta.fileId !== expectedFileId) { + errors.push(`FileId mismatch - expected: ${expectedFileId}, got: ${chunkMeta.fileId}`); + } + + // Validate chunk size + if (chunkMeta.chunkSize <= 0) { + errors.push(`Invalid chunk size: ${chunkMeta.chunkSize}`); + } + + // Check if chunk index is reasonable + if (chunkMeta.chunkIndex < 0) { + errors.push(`Invalid chunk index: ${chunkMeta.chunkIndex}`); + } + + // Validate total chunks (with resume consideration) + const startChunkIndex = ReceptionConfig.getChunkIndexFromOffset(initialOffset); + const calculatedExpected = chunkMeta.totalChunks - startChunkIndex; + + if (chunkMeta.totalChunks !== expectedChunksCount && calculatedExpected !== expectedChunksCount) { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG-CHUNKS] Chunk count info - fileTotal: ${chunkMeta.totalChunks}, currentExpected: ${expectedChunksCount}, calculatedExpected: ${calculatedExpected}, startChunk: ${startChunkIndex}` + ); + } + } + + return { + isValid: errors.length === 0, + errors, + }; + } + + /** + * Check if chunk index is within valid range + */ + isChunkIndexValid( + relativeChunkIndex: number, + expectedChunksCount: number + ): boolean { + return relativeChunkIndex >= 0 && relativeChunkIndex < expectedChunksCount; + } + + /** + * Log chunk processing details (for debugging) + */ + logChunkDetails( + result: ChunkProcessingResult, + expectedChunksCount: number, + writerExpectedIndex?: number + ): void { + if (!ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + return; + } + + const { chunkMeta, absoluteChunkIndex, relativeChunkIndex } = result; + const lastFewChunks = relativeChunkIndex >= expectedChunksCount - 5; + + if (absoluteChunkIndex <= 970 || lastFewChunks) { + postLogToBackend( + `[DEBUG-CHUNKS] 📦 Chunk #${absoluteChunkIndex} received - relative:${relativeChunkIndex}, size:${chunkMeta.chunkSize}${ + writerExpectedIndex !== undefined ? `, writerExpects:${writerExpectedIndex}` : '' + }, isLastFew:${lastFewChunks}` + ); + } + } + + /** + * Calculate completion statistics + */ + calculateCompletionStats( + chunks: (ArrayBuffer | null)[], + expectedChunksCount: number, + expectedSize: number + ): { + sequencedCount: number; + currentTotalSize: number; + isSequencedComplete: boolean; + sizeComplete: boolean; + isDataComplete: boolean; + } { + // Calculate current actual total received size + const currentTotalSize = chunks.reduce((sum, chunk) => { + return sum + (chunk instanceof ArrayBuffer ? chunk.byteLength : 0); + }, 0); + + // Count sequentially received chunks + let sequencedCount = 0; + for (let i = 0; i < expectedChunksCount; i++) { + if (chunks[i] instanceof ArrayBuffer) { + sequencedCount++; + } + } + + const isSequencedComplete = sequencedCount === expectedChunksCount; + const sizeComplete = currentTotalSize >= expectedSize; + const isDataComplete = isSequencedComplete && sizeComplete; + + return { + sequencedCount, + currentTotalSize, + isSequencedComplete, + sizeComplete, + isDataComplete, + }; + } + + /** + * Log completion check details (for debugging) + */ + logCompletionCheck( + fileName: string, + stats: { + sequencedCount: number; + expectedChunksCount: number; + currentTotalSize: number; + expectedSize: number; + isDataComplete: boolean; + }, + chunks: (ArrayBuffer | null)[], + initialOffset: number + ): void { + if (!ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + return; + } + + const { sequencedCount, expectedChunksCount, currentTotalSize, expectedSize, isDataComplete } = stats; + + // Only log at key moments to reduce noise + if ( + isDataComplete || + sequencedCount % ReceptionConfig.DEBUG_CONFIG.PROGRESS_LOG_INTERVAL === 0 || + sequencedCount > expectedChunksCount - 10 + ) { + // Check last few chunks status + const lastChunkIndex = expectedChunksCount - 1; + const lastFewChunks = []; + const startChunkIndex = ReceptionConfig.getChunkIndexFromOffset(initialOffset); + + for (let i = Math.max(0, lastChunkIndex - 3); i <= lastChunkIndex; i++) { + const chunk = chunks[i]; + const exists = chunk instanceof ArrayBuffer; + const size = exists ? (chunk as ArrayBuffer).byteLength : 0; + const absoluteIndex = startChunkIndex + i; + lastFewChunks.push(`rel#${i}(abs#${absoluteIndex}):${exists}(${size})`); + } + + postLogToBackend(`[DEBUG-COMPLETE] Check completion - file:${fileName}`); + postLogToBackend( + `[DEBUG-COMPLETE] Chunks: received:${sequencedCount}/${expectedChunksCount}, isSequenceComplete:${stats.sequencedCount === expectedChunksCount}` + ); + postLogToBackend( + `[DEBUG-COMPLETE] Size: current:${currentTotalSize}, expected:${expectedSize}, sizeComplete:${currentTotalSize >= expectedSize}, diff:${ + expectedSize - currentTotalSize + }` + ); + postLogToBackend( + `[DEBUG-COMPLETE] LastChunks: ${lastFewChunks.join(", ")}` + ); + postLogToBackend( + `[DEBUG-COMPLETE] IsDataComplete: ${isDataComplete}` + ); + } + } +} \ No newline at end of file diff --git a/frontend/lib/receive/FileAssembler.ts b/frontend/lib/receive/FileAssembler.ts new file mode 100644 index 0000000..0eb2f45 --- /dev/null +++ b/frontend/lib/receive/FileAssembler.ts @@ -0,0 +1,280 @@ +import { CustomFile, fileMetadata } from "@/types/webrtc"; +import { ReceptionConfig } from "./ReceptionConfig"; +import { postLogToBackend } from "@/app/config/api"; + +const developmentEnv = process.env.NODE_ENV; + +/** + * 🚀 File assembly result interface + */ +export interface FileAssemblyResult { + file: CustomFile; + totalChunkSize: number; + validChunks: number; + storeUpdated: boolean; +} + +/** + * 🚀 File assembler + * Handles in-memory file assembly and validation + */ +export class FileAssembler { + /** + * Assemble file from chunks in memory + */ + async assembleFileFromChunks( + chunks: (ArrayBuffer | null)[], + meta: fileMetadata, + currentFolderName: string | null, + onFileReceived?: (file: CustomFile) => Promise + ): Promise { + // Validate and count chunks + let totalChunkSize = 0; + let validChunks = 0; + + chunks.forEach((chunk, index) => { + if (chunk instanceof ArrayBuffer) { + validChunks++; + totalChunkSize += chunk.byteLength; + } + }); + + // Final verification + const sizeDifference = meta.size - totalChunkSize; + if (Math.abs(sizeDifference) > ReceptionConfig.VALIDATION_CONFIG.MAX_SIZE_DIFFERENCE_BYTES) { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG] ❌ SIZE_MISMATCH - difference: ${sizeDifference} bytes (threshold: ${ReceptionConfig.VALIDATION_CONFIG.MAX_SIZE_DIFFERENCE_BYTES})` + ); + } + } + + // Create file blob from valid chunks + const validChunkBuffers = chunks.filter( + (chunk) => chunk instanceof ArrayBuffer + ) as ArrayBuffer[]; + + const fileBlob = new Blob(validChunkBuffers, { + type: meta.fileType, + }); + + // Create File object + const file = new File([fileBlob], meta.name, { + type: meta.fileType, + }); + + // Create CustomFile with additional properties + const customFile = Object.assign(file, { + fullName: meta.fullName, + folderName: currentFolderName, + }) as CustomFile; + + // Store the file if callback is provided + let storeUpdated = false; + if (onFileReceived) { + await onFileReceived(customFile); + await Promise.resolve(); + await new Promise((resolve) => setTimeout(() => resolve(), 0)); + storeUpdated = true; + } + + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG] ✅ File assembled - ${meta.name}, chunks: ${validChunks}/${chunks.length}, size: ${totalChunkSize}/${meta.size}, stored: ${storeUpdated}` + ); + } + + return { + file: customFile, + totalChunkSize, + validChunks, + storeUpdated, + }; + } + + /** + * Validate file assembly completeness + */ + validateAssembly( + chunks: (ArrayBuffer | null)[], + expectedSize: number, + expectedChunksCount: number + ): { + isComplete: boolean; + validChunks: number; + totalSize: number; + missingChunks: number[]; + sizeDifference: number; + } { + let totalSize = 0; + let validChunks = 0; + const missingChunks: number[] = []; + + chunks.forEach((chunk, index) => { + if (chunk instanceof ArrayBuffer) { + validChunks++; + totalSize += chunk.byteLength; + } else { + missingChunks.push(index); + } + }); + + const sizeDifference = expectedSize - totalSize; + const isComplete = + validChunks === expectedChunksCount && + Math.abs(sizeDifference) <= ReceptionConfig.VALIDATION_CONFIG.MAX_SIZE_DIFFERENCE_BYTES; + + return { + isComplete, + validChunks, + totalSize, + missingChunks, + sizeDifference, + }; + } + + /** + * Get assembly statistics for debugging + */ + getAssemblyStats(chunks: (ArrayBuffer | null)[]): { + totalChunks: number; + validChunks: number; + nullChunks: number; + totalSize: number; + averageChunkSize: number; + firstNullIndex: number | null; + lastValidIndex: number | null; + } { + let validChunks = 0; + let totalSize = 0; + let firstNullIndex: number | null = null; + let lastValidIndex: number | null = null; + + chunks.forEach((chunk, index) => { + if (chunk instanceof ArrayBuffer) { + validChunks++; + totalSize += chunk.byteLength; + lastValidIndex = index; + } else { + if (firstNullIndex === null) { + firstNullIndex = index; + } + } + }); + + const averageChunkSize = validChunks > 0 ? totalSize / validChunks : 0; + + return { + totalChunks: chunks.length, + validChunks, + nullChunks: chunks.length - validChunks, + totalSize, + averageChunkSize, + firstNullIndex, + lastValidIndex, + }; + } + + /** + * Create file download URL for in-memory files + */ + createDownloadUrl(file: File): string { + return URL.createObjectURL(file); + } + + /** + * Revoke file download URL to free memory + */ + revokeDownloadUrl(url: string): void { + URL.revokeObjectURL(url); + } + + /** + * Get file type information + */ + getFileTypeInfo(file: File): { + mimeType: string; + extension: string; + category: 'image' | 'video' | 'audio' | 'document' | 'archive' | 'other'; + } { + const mimeType = file.type || 'application/octet-stream'; + const extension = file.name.split('.').pop()?.toLowerCase() || ''; + + let category: 'image' | 'video' | 'audio' | 'document' | 'archive' | 'other' = 'other'; + + if (mimeType.startsWith('image/')) { + category = 'image'; + } else if (mimeType.startsWith('video/')) { + category = 'video'; + } else if (mimeType.startsWith('audio/')) { + category = 'audio'; + } else if ( + mimeType.includes('text/') || + mimeType.includes('application/pdf') || + mimeType.includes('application/msword') || + mimeType.includes('application/vnd.openxmlformats') + ) { + category = 'document'; + } else if ( + mimeType.includes('zip') || + mimeType.includes('rar') || + mimeType.includes('tar') || + mimeType.includes('gzip') + ) { + category = 'archive'; + } + + return { + mimeType, + extension, + category, + }; + } + + /** + * Estimate memory usage for file assembly + */ + estimateMemoryUsage(chunks: (ArrayBuffer | null)[]): { + chunkMemoryUsage: number; + estimatedBlobMemory: number; + totalEstimatedMemory: number; + } { + const chunkMemoryUsage = chunks.reduce((sum, chunk) => { + return sum + (chunk instanceof ArrayBuffer ? chunk.byteLength : 0); + }, 0); + + // Blob creation might temporarily double memory usage + const estimatedBlobMemory = chunkMemoryUsage; + const totalEstimatedMemory = chunkMemoryUsage + estimatedBlobMemory; + + return { + chunkMemoryUsage, + estimatedBlobMemory, + totalEstimatedMemory, + }; + } + + /** + * Check if file should be assembled in memory or streamed to disk + */ + shouldAssembleInMemory( + fileSize: number, + hasSaveDirectory: boolean, + availableMemory?: number + ): boolean { + // If we have a save directory and file is large, prefer disk + if (hasSaveDirectory && fileSize >= ReceptionConfig.FILE_CONFIG.LARGE_FILE_THRESHOLD) { + return false; + } + + // If available memory is provided, check if we have enough + if (availableMemory !== undefined) { + // Need roughly 2x file size for assembly process + const requiredMemory = fileSize * 2; + return availableMemory > requiredMemory; + } + + // Default: assemble in memory for smaller files + return fileSize < ReceptionConfig.FILE_CONFIG.LARGE_FILE_THRESHOLD; + } +} \ No newline at end of file diff --git a/frontend/lib/receive/FileReceiveOrchestrator.ts b/frontend/lib/receive/FileReceiveOrchestrator.ts new file mode 100644 index 0000000..93c25d0 --- /dev/null +++ b/frontend/lib/receive/FileReceiveOrchestrator.ts @@ -0,0 +1,666 @@ +import WebRTC_Recipient from "../webrtc_Recipient"; +import { CustomFile, fileMetadata } from "@/types/webrtc"; +import { ReceptionStateManager } from "./ReceptionStateManager"; +import { MessageProcessor, MessageProcessorDelegate } from "./MessageProcessor"; +import { ChunkProcessor, ChunkProcessingResult } from "./ChunkProcessor"; +import { StreamingFileWriter, SequencedDiskWriter } from "./StreamingFileWriter"; +import { FileAssembler } from "./FileAssembler"; +import { ProgressReporter, ProgressCallback } from "./ProgressReporter"; +import { ReceptionConfig } from "./ReceptionConfig"; +import { postLogToBackend } from "@/app/config/api"; + +const developmentEnv = process.env.NODE_ENV; + +/** + * 🚀 File receive orchestrator + * Main coordinator that integrates all reception modules + */ +export class FileReceiveOrchestrator implements MessageProcessorDelegate { + private stateManager: ReceptionStateManager; + private messageProcessor: MessageProcessor; + private chunkProcessor: ChunkProcessor; + private streamingFileWriter: StreamingFileWriter; + private fileAssembler: FileAssembler; + private progressReporter: ProgressReporter; + + // Callbacks + public onFileMetaReceived: ((meta: fileMetadata) => void) | undefined = undefined; + public onStringReceived: ((str: string) => void) | undefined = undefined; + public onFileReceived: ((file: CustomFile) => Promise) | undefined = undefined; + + constructor(private webrtcConnection: WebRTC_Recipient) { + // Initialize all components + this.stateManager = new ReceptionStateManager(); + this.chunkProcessor = new ChunkProcessor(); + this.streamingFileWriter = new StreamingFileWriter(); + this.fileAssembler = new FileAssembler(); + this.progressReporter = new ProgressReporter(this.stateManager); + this.messageProcessor = new MessageProcessor( + this.stateManager, + webrtcConnection, + { + onFileMetaReceived: (meta: fileMetadata) => { + if (this.onFileMetaReceived) { + this.onFileMetaReceived(meta); + } + }, + onStringReceived: (str: string) => { + if (this.onStringReceived) { + this.onStringReceived(str); + } + }, + log: this.log.bind(this) + } + ); + + // Set up data handler + this.setupDataHandler(); + + this.log("log", "FileReceiveOrchestrator initialized"); + } + + // ===== Public API ===== + + /** + * Set progress callback + */ + public setProgressCallback(callback: ProgressCallback): void { + this.progressReporter.setProgressCallback(callback); + } + + /** + * Set save directory + */ + public setSaveDirectory(directory: FileSystemDirectoryHandle): Promise { + this.stateManager.setSaveDirectory(directory); + this.streamingFileWriter.setSaveDirectory(directory); + return Promise.resolve(); + } + + /** + * Request a single file from the peer + */ + public async requestFile(fileId: string, singleFile = true): Promise { + const activeReception = this.stateManager.getActiveFileReception(); + if (activeReception) { + this.log("warn", "Another file reception is already in progress."); + return; + } + + if (singleFile) { + this.stateManager.setCurrentFolderName(null); + } + + const fileInfo = this.stateManager.getFileMetadata(fileId); + if (!fileInfo) { + this.fireError("File info not found for the requested fileId", { fileId }); + return; + } + + const shouldSaveToDisk = ReceptionConfig.shouldSaveToDisk( + fileInfo.size, + this.streamingFileWriter.hasSaveDirectory() + ); + + // Set save type at the beginning to prevent race conditions + this.stateManager.setSaveType(fileInfo.fileId, shouldSaveToDisk); + const currentFolderName = this.stateManager.getCurrentFolderName(); + if (currentFolderName) { + this.stateManager.setSaveType(currentFolderName, shouldSaveToDisk); + } + + let offset = 0; + if (shouldSaveToDisk && this.streamingFileWriter.hasSaveDirectory()) { + try { + offset = await this.streamingFileWriter.getPartialFileSize( + fileInfo.name, + fileInfo.fullName + ); + + if (offset === fileInfo.size) { + this.log("log", "File already fully downloaded.", { fileId }); + this.progressReporter.reportFileComplete(fileId); + return; + } + this.log("log", `Resuming file from offset: ${offset}`, { fileId }); + } catch (e) { + this.log("log", "Partial file not found, starting from scratch.", { fileId }); + offset = 0; + } + } + + const expectedChunksCount = ReceptionConfig.calculateExpectedChunks( + fileInfo.size, + offset + ); + + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + const totalChunks = ReceptionConfig.calculateTotalChunks(fileInfo.size); + const startChunkIndex = ReceptionConfig.getChunkIndexFromOffset(offset); + + postLogToBackend(`[DEBUG-CHUNKS] File: ${fileInfo.name}`); + postLogToBackend( + `[DEBUG-CHUNKS] File size: ${fileInfo.size}, offset: ${offset}` + ); + postLogToBackend( + `[DEBUG-CHUNKS] Total chunks in file: ${totalChunks} (0-${totalChunks - 1})` + ); + postLogToBackend(`[DEBUG-CHUNKS] Start chunk index: ${startChunkIndex}`); + postLogToBackend( + `[DEBUG-CHUNKS] Expected chunks: ${expectedChunksCount}` + ); + } + + const receptionPromise = this.stateManager.startFileReception( + fileInfo, + expectedChunksCount, + offset + ); + + if (shouldSaveToDisk) { + await this.createDiskWriteStream(fileInfo, offset); + } + + // Send file request + const success = this.messageProcessor.sendFileRequest(fileId, offset); + if (!success) { + this.stateManager.failFileReception(new Error("Failed to send file request")); + return; + } + + return receptionPromise; + } + + /** + * Request all files belonging to a folder from the peer + */ + public async requestFolder(folderName: string): Promise { + const folderProgress = this.stateManager.getFolderProgress(folderName); + if (!folderProgress || folderProgress.fileIds.length === 0) { + this.log("warn", "No files found for the requested folder.", { folderName }); + return; + } + + // Pre-calculate total size of already downloaded parts + let initialFolderReceivedSize = 0; + if (this.streamingFileWriter.hasSaveDirectory()) { + for (const fileId of folderProgress.fileIds) { + const fileInfo = this.stateManager.getFileMetadata(fileId); + if (fileInfo) { + try { + const partialSize = await this.streamingFileWriter.getPartialFileSize( + fileInfo.name, + fileInfo.fullName + ); + initialFolderReceivedSize += partialSize; + } catch (e) { + // File doesn't exist, so its size is 0 + } + } + } + } + + this.stateManager.setFolderReceivedSize(folderName, initialFolderReceivedSize); + this.log( + "log", + `Requesting folder, initial received size: ${initialFolderReceivedSize}`, + { folderName } + ); + + this.stateManager.setCurrentFolderName(folderName); + + for (const fileId of folderProgress.fileIds) { + try { + await this.requestFile(fileId, false); + } catch (error) { + this.fireError( + `Failed to receive file ${fileId} in folder ${folderName}`, + { error } + ); + break; + } + } + + this.stateManager.setCurrentFolderName(null); + + // Send folder completion message + const completedFileIds = folderProgress.fileIds.filter(() => true); // Assume all succeeded + + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG] 📁 All files in folder completed - ${folderName}, files: ${completedFileIds.length}/${folderProgress.fileIds.length}` + ); + } + + this.messageProcessor.sendFolderReceiveComplete(folderName, completedFileIds, true); + } + + // ===== MessageProcessorDelegate Implementation ===== + + // Note: These are implemented as properties, not methods, to avoid infinite recursion + + public log( + level: "log" | "warn" | "error", + message: string, + context?: Record + ): void { + const prefix = `[FileReceiveOrchestrator]`; + console[level](prefix, message, context || ""); + } + + // ===== Internal Methods ===== + + /** + * Set up data handler + */ + private setupDataHandler(): void { + this.webrtcConnection.onDataReceived = async (data, peerId) => { + const binaryData = await this.messageProcessor.handleReceivedMessage(data, peerId); + + if (binaryData) { + // Handle binary chunk data + await this.handleBinaryChunkData(binaryData); + } + }; + } + + /** + * Handle binary chunk data + */ + private async handleBinaryChunkData(data: any): Promise { + const activeReception = this.stateManager.getActiveFileReception(); + if (!activeReception) { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG] ERROR: Received file chunk but no active file reception!` + ); + } + this.fireError("Received a file chunk without an active file reception."); + return; + } + + // Convert to ArrayBuffer + const arrayBuffer = await this.chunkProcessor.convertToArrayBuffer(data); + if (!arrayBuffer) { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG] ERROR: Failed to convert binary data to ArrayBuffer` + ); + } + this.fireError("Received unsupported binary data format", { + dataType: Object.prototype.toString.call(data), + }); + return; + } + + await this.handleEmbeddedChunkPacket(arrayBuffer); + } + + /** + * Handle embedded chunk packet + */ + private async handleEmbeddedChunkPacket(arrayBuffer: ArrayBuffer): Promise { + const parsed = this.chunkProcessor.parseEmbeddedChunkPacket(arrayBuffer); + if (!parsed) { + this.fireError("Failed to parse embedded chunk packet"); + return; + } + + const { chunkMeta, chunkData } = parsed; + const reception = this.stateManager.getActiveFileReception(); + if (!reception) { + console.log( + `[FileReceiveOrchestrator] Ignoring chunk ${chunkMeta.chunkIndex} - file reception already closed` + ); + return; + } + + // Validate chunk + const validation = this.chunkProcessor.validateChunk( + chunkMeta, + reception.meta.fileId, + reception.expectedChunksCount, + reception.initialOffset + ); + + if (!validation.isValid) { + this.log("warn", "Chunk validation failed", { + errors: validation.errors, + chunkIndex: chunkMeta.chunkIndex, + }); + return; + } + + // Process chunk indices + const result = this.chunkProcessor.processReceivedChunk( + chunkMeta, + chunkData, + reception.initialOffset + ); + + if (!result) { + this.fireError("Failed to process received chunk"); + return; + } + + // Check if chunk index is valid + if (!this.chunkProcessor.isChunkIndexValid( + result.relativeChunkIndex, + reception.expectedChunksCount + )) { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG-CHUNKS] ❌ Invalid relative chunk index - absolute:${result.absoluteChunkIndex}, relative:${result.relativeChunkIndex}, arraySize:${reception.chunks.length}` + ); + } + return; + } + + // Store chunk + reception.chunks[result.relativeChunkIndex] = result.chunkData; + reception.chunkSequenceMap.set(result.absoluteChunkIndex, true); + reception.receivedChunksCount++; + + // Update progress + this.progressReporter.updateFileProgress( + result.chunkData.byteLength, + reception.meta.fileId, + reception.meta.size + ); + + // Handle disk writing if needed + if (reception.sequencedWriter) { + this.chunkProcessor.logChunkDetails( + result, + reception.expectedChunksCount, + reception.sequencedWriter.expectedIndex + ); + + await reception.sequencedWriter.writeChunk( + result.absoluteChunkIndex, + result.chunkData + ); + } + + await this.checkAndAutoFinalize(); + } + + /** + * Check and auto-finalize file reception + */ + private async checkAndAutoFinalize(): Promise { + const reception = this.stateManager.getActiveFileReception(); + if (!reception || reception.isFinalized) return; + + const expectedSize = reception.meta.size - reception.initialOffset; + const stats = this.chunkProcessor.calculateCompletionStats( + reception.chunks, + reception.expectedChunksCount, + expectedSize + ); + + // Log completion check details + this.chunkProcessor.logCompletionCheck( + reception.meta.name, + { + sequencedCount: stats.sequencedCount, + expectedChunksCount: reception.expectedChunksCount, + currentTotalSize: stats.currentTotalSize, + expectedSize, + isDataComplete: stats.isDataComplete, + }, + reception.chunks, + reception.initialOffset + ); + + if (stats.isDataComplete) { + reception.isFinalized = true; + + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG-COMPLETE] ✅ Starting finalization - isDataComplete:${stats.isDataComplete}` + ); + } + + try { + await this.finalizeFileReceive(); + this.stateManager.completeFileReception(); + } catch (error) { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend(`[DEBUG] ❌ Auto-finalize ERROR: ${error}`); + } + this.stateManager.failFileReception(error); + } + } + } + + /** + * Finalize file reception + */ + private async finalizeFileReceive(): Promise { + const reception = this.stateManager.getActiveFileReception(); + if (!reception) return; + + if (reception.writeStream) { + await this.finalizeLargeFileReceive(); + } else { + await this.finalizeMemoryFileReceive(); + } + } + + /** + * Finalize large file reception (disk-based) + */ + private async finalizeLargeFileReceive(): Promise { + const reception = this.stateManager.getActiveFileReception(); + if (!reception?.writeStream || !reception.fileHandle) { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG-FINALIZE] ❌ Cannot finalize - missing writeStream:${!!reception?.writeStream} or fileHandle:${!!reception?.fileHandle}` + ); + } + return; + } + + try { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG-FINALIZE] 🚀 Starting finalization for ${reception.meta.name}` + ); + } + + // Finalize using StreamingFileWriter + if (reception.sequencedWriter && reception.writeStream) { + await this.streamingFileWriter.finalizeWrite( + reception.sequencedWriter, + reception.writeStream, + reception.meta.name + ); + } + + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG-FINALIZE] ✅ LARGE_FILE finalized successfully - ${reception.meta.name}` + ); + } + } catch (error) { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG-FINALIZE] ❌ Error during finalization: ${error}` + ); + } + this.fireError("Error finalizing large file", { error }); + } + } + + /** + * Finalize memory file reception + */ + private async finalizeMemoryFileReceive(): Promise { + const reception = this.stateManager.getActiveFileReception(); + if (!reception) return; + + const currentFolderName = this.stateManager.getCurrentFolderName(); + const result = await this.fileAssembler.assembleFileFromChunks( + reception.chunks, + reception.meta, + currentFolderName, + this.onFileReceived + ); + + // Send completion confirmation + this.messageProcessor.sendFileReceiveComplete( + reception.meta.fileId, + result.totalChunkSize, + result.validChunks, + result.storeUpdated + ); + } + + /** + * Create disk write stream + */ + private async createDiskWriteStream( + meta: fileMetadata, + offset: number + ): Promise { + try { + const { fileHandle, writeStream, sequencedWriter } = + await this.streamingFileWriter.createWriteStream( + meta.name, + meta.fullName, + offset + ); + + this.stateManager.updateActiveFileReception({ + fileHandle, + writeStream, + sequencedWriter, + }); + } catch (err) { + this.fireError("Failed to create file on disk", { + err, + fileName: meta.name, + }); + } + } + + /** + * Error handling + */ + private fireError(message: string, context?: Record) { + if (this.webrtcConnection.fireError) { + // @ts-ignore + this.webrtcConnection.fireError(message, { + ...context, + component: "FileReceiveOrchestrator", + }); + } else { + this.log("error", message, context); + } + + const reception = this.stateManager.getActiveFileReception(); + if (reception) { + // Clean up resources on error + if (reception.sequencedWriter) { + reception.sequencedWriter.close().catch((err: any) => { + this.log( + "error", + "Error closing sequenced writer during error cleanup", + { err } + ); + }); + } + + this.stateManager.failFileReception(new Error(message)); + } + } + + // ===== Lifecycle Management ===== + + /** + * Graceful shutdown + */ + public gracefulShutdown(reason: string = "CONNECTION_LOST"): void { + this.log("log", `Graceful shutdown initiated: ${reason}`); + + const reception = this.stateManager.getActiveFileReception(); + if (reception?.sequencedWriter && reception?.writeStream) { + this.log( + "log", + "Attempting to gracefully close streams on shutdown." + ); + + // Close sequenced writer and write stream + reception.sequencedWriter.close().catch((err: any) => { + this.log("error", "Error closing sequenced writer during graceful shutdown", { err }); + }); + + reception.writeStream.close().catch((err: any) => { + this.log("error", "Error closing stream during graceful shutdown", { err }); + }); + } + + this.stateManager.gracefulCleanup(); + this.log("log", "Graceful shutdown completed"); + } + + /** + * Force reset all internal states + */ + public forceReset(): void { + this.log("log", "Force resetting FileReceiveOrchestrator state"); + + const reception = this.stateManager.getActiveFileReception(); + if (reception?.sequencedWriter && reception?.writeStream) { + reception.sequencedWriter.close().catch(console.error); + reception.writeStream.close().catch(console.error); + } + + this.stateManager.forceReset(); + this.progressReporter.resetAllProgress(); + this.log("log", "FileReceiveOrchestrator state force reset completed"); + } + + /** + * Get transfer statistics + */ + public getTransferStats() { + return { + stateManager: this.stateManager.getStateStats(), + progressReporter: this.progressReporter.getProgressStats(), + messageProcessor: this.messageProcessor.getMessageStats(), + }; + } + + /** + * Get save type information (for backward compatibility) + */ + public getSaveType(): Record { + return this.stateManager.saveType; + } + + /** + * Get pending files metadata (for backward compatibility) + */ + public getPendingFilesMeta(): Map { + return this.stateManager.getAllFileMetadata(); + } + + /** + * Get folder progresses (for backward compatibility) + */ + public getFolderProgresses(): Record { + return this.stateManager.getAllFolderProgresses(); + } + + /** + * Clean up all resources + */ + public cleanup(): void { + this.stateManager.gracefulCleanup(); + this.progressReporter.cleanup(); + this.messageProcessor.cleanup(); + this.log("log", "FileReceiveOrchestrator cleaned up"); + } +} \ No newline at end of file diff --git a/frontend/lib/receive/MessageProcessor.ts b/frontend/lib/receive/MessageProcessor.ts new file mode 100644 index 0000000..8c902a7 --- /dev/null +++ b/frontend/lib/receive/MessageProcessor.ts @@ -0,0 +1,302 @@ +import { + WebRTCMessage, + fileMetadata, + StringMetadata, + StringChunk, + FileRequest, + FileReceiveComplete, + FolderReceiveComplete, + FileHandlers, +} from "@/types/webrtc"; +import { ReceptionStateManager } from "./ReceptionStateManager"; +import { ReceptionConfig } from "./ReceptionConfig"; +import { postLogToBackend } from "@/app/config/api"; +import WebRTC_Recipient from "../webrtc_Recipient"; + +const developmentEnv = process.env.NODE_ENV; + +/** + * 🚀 Message processor delegate interface + */ +export interface MessageProcessorDelegate { + onFileMetaReceived?: (meta: fileMetadata) => void; + onStringReceived?: (str: string) => void; + log(level: "log" | "warn" | "error", message: string, context?: Record): void; +} + +/** + * 🚀 Message processor + * Handles WebRTC message routing, processing, and communication + */ +export class MessageProcessor { + private fileHandlers: FileHandlers; + + constructor( + private stateManager: ReceptionStateManager, + private webrtcConnection: WebRTC_Recipient, + private delegate: MessageProcessorDelegate + ) { + this.fileHandlers = { + string: this.handleReceivedStringChunk.bind(this), + stringMetadata: this.handleStringMetadata.bind(this), + fileMeta: this.handleFileMetadata.bind(this), + }; + } + + /** + * Handle received WebRTC message + */ + async handleReceivedMessage( + data: string | ArrayBuffer | any, + peerId: string + ): Promise { + this.stateManager.setCurrentPeerId(peerId); + + if (typeof data === "string") { + try { + const parsedData = JSON.parse(data) as WebRTCMessage; + const handler = this.fileHandlers[parsedData.type as keyof FileHandlers]; + + if (handler) { + await handler(parsedData as any, peerId); + } else { + this.delegate.log( + "warn", + `Handler not found for message type: ${parsedData.type}`, + { peerId } + ); + } + return null; // String messages don't return binary data + } catch (error) { + this.delegate.log("error", "Error parsing received JSON data", { error, peerId }); + return null; + } + } else { + // Return binary data for chunk processing + return data; + } + } + + /** + * Handle file metadata message + */ + private handleFileMetadata(metadata: fileMetadata): void { + const isNewMetadata = this.stateManager.addFileMetadata(metadata); + + if (!isNewMetadata) { + return; // Ignore if already received + } + + if (this.delegate.onFileMetaReceived) { + this.delegate.onFileMetaReceived(metadata); + } else { + this.delegate.log( + "error", + "onFileMetaReceived callback not set", + { fileId: metadata.fileId } + ); + } + } + + /** + * Handle string metadata message + */ + private handleStringMetadata(metadata: StringMetadata): void { + this.stateManager.startStringReception(metadata.length); + } + + /** + * Handle received string chunk message + */ + private handleReceivedStringChunk(data: StringChunk): void { + const activeStringReception = this.stateManager.getActiveStringReception(); + if (!activeStringReception) { + this.delegate.log("warn", "Received string chunk without active reception"); + return; + } + + this.stateManager.updateStringReceptionChunk(data.index, data.chunk); + + // Check if string reception is complete + if (activeStringReception.receivedChunks === data.total) { + const fullString = this.stateManager.completeStringReception(); + if (fullString && this.delegate.onStringReceived) { + this.delegate.onStringReceived(fullString); + } + } + } + + /** + * Send file request message + */ + sendFileRequest(fileId: string, offset: number = 0): boolean { + const peerId = this.stateManager.getCurrentPeerId(); + if (!peerId) { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG] ERROR: Cannot send fileRequest - no peerId available!` + ); + } + return false; + } + + const request: FileRequest = { type: "fileRequest", fileId, offset }; + const success = this.webrtcConnection.sendData(JSON.stringify(request), peerId); + + if (success) { + this.delegate.log("log", "Sent fileRequest", { request, peerId }); + } else { + this.delegate.log("error", "Failed to send fileRequest", { request, peerId }); + } + + return success; + } + + /** + * Send file receive complete message + */ + sendFileReceiveComplete( + fileId: string, + receivedSize: number, + receivedChunks: number, + storeUpdated: boolean + ): boolean { + const peerId = this.stateManager.getCurrentPeerId(); + if (!peerId) { + this.delegate.log("warn", "Cannot send file receive complete - no peer ID"); + return false; + } + + const completeMessage: FileReceiveComplete = { + type: "fileReceiveComplete", + fileId, + receivedSize, + receivedChunks, + storeUpdated, + }; + + const success = this.webrtcConnection.sendData( + JSON.stringify(completeMessage), + peerId + ); + + if (success) { + this.delegate.log("log", "Sent file receive complete", { + fileId, + receivedSize, + receivedChunks, + storeUpdated, + }); + } else { + this.delegate.log("error", "Failed to send file receive complete", { + fileId, + peerId, + }); + } + + return success; + } + + /** + * Send folder receive complete message + */ + sendFolderReceiveComplete( + folderName: string, + completedFileIds: string[], + allStoreUpdated: boolean + ): boolean { + const peerId = this.stateManager.getCurrentPeerId(); + if (!peerId) { + this.delegate.log("warn", "Cannot send folder receive complete - no peer ID"); + return false; + } + + const completeMessage: FolderReceiveComplete = { + type: "folderReceiveComplete", + folderName, + completedFileIds, + allStoreUpdated, + }; + + const success = this.webrtcConnection.sendData( + JSON.stringify(completeMessage), + peerId + ); + + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG] 📤 Sent folderReceiveComplete - folderName: ${folderName}, completedFiles: ${completedFileIds.length}, allStoreUpdated: ${allStoreUpdated}, success: ${success}` + ); + } + + if (success) { + this.delegate.log("log", "Sent folder receive complete", { + folderName, + completedFiles: completedFileIds.length, + allStoreUpdated, + }); + } else { + this.delegate.log("error", "Failed to send folder receive complete", { + folderName, + peerId, + }); + } + + return success; + } + + /** + * Add Firefox compatibility delay + */ + async addFirefoxDelay(): Promise { + await new Promise((resolve) => + setTimeout(resolve, ReceptionConfig.NETWORK_CONFIG.FIREFOX_COMPATIBILITY_DELAY) + ); + } + + /** + * Get message processing statistics + */ + getMessageStats(): { + handledMessages: number; + lastMessageTime: number | null; + currentPeerId: string; + } { + return { + handledMessages: 0, // TODO: Implement message counting if needed + lastMessageTime: null, // TODO: Record last message time if needed + currentPeerId: this.stateManager.getCurrentPeerId(), + }; + } + + /** + * Check if connection is available + */ + isConnectionAvailable(): boolean { + const peerId = this.stateManager.getCurrentPeerId(); + return !!peerId && !!this.webrtcConnection; + } + + /** + * Get current peer connection info + */ + getPeerConnectionInfo(): { + peerId: string; + isConnected: boolean; + } { + const peerId = this.stateManager.getCurrentPeerId(); + return { + peerId, + isConnected: this.isConnectionAvailable(), + }; + } + + /** + * Clean up resources + */ + cleanup(): void { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend("[DEBUG] 🧹 MessageProcessor cleaned up"); + } + } +} \ No newline at end of file diff --git a/frontend/lib/receive/ProgressReporter.ts b/frontend/lib/receive/ProgressReporter.ts new file mode 100644 index 0000000..89351e6 --- /dev/null +++ b/frontend/lib/receive/ProgressReporter.ts @@ -0,0 +1,309 @@ +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; + folderProgress: Record; + 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(); + private folderProgressMap = new Map(); + private lastProgressUpdate = new Map(); + + 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 = {}; + this.fileProgressMap.forEach((progress, fileId) => { + fileProgress[fileId] = progress; + }); + + const folderProgress: Record = {}; + 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"); + } + } +} \ No newline at end of file diff --git a/frontend/lib/receive/ReceptionConfig.ts b/frontend/lib/receive/ReceptionConfig.ts new file mode 100644 index 0000000..b96d1e1 --- /dev/null +++ b/frontend/lib/receive/ReceptionConfig.ts @@ -0,0 +1,74 @@ +/** + * 🚀 Reception configuration management + * Centralized configuration for file reception parameters + */ + +export class ReceptionConfig { + // File size thresholds + static readonly FILE_CONFIG = { + LARGE_FILE_THRESHOLD: 1 * 1024 * 1024 * 1024, // 1GB - files larger than this will be saved to disk + CHUNK_SIZE: 65536, // 64KB standard chunk size + }; + + // Buffer management + static readonly BUFFER_CONFIG = { + MAX_BUFFER_SIZE: 100, // Buffer up to 100 chunks (approximately 6.4MB) + SEQUENTIAL_FLUSH_THRESHOLD: 10, // Start flushing when this many sequential chunks are available + }; + + // Performance and debugging + static readonly DEBUG_CONFIG = { + ENABLE_CHUNK_LOGGING: process.env.NODE_ENV === "development", + ENABLE_PROGRESS_LOGGING: process.env.NODE_ENV === "development", + PROGRESS_LOG_INTERVAL: 500, // Log progress every N chunks + COMPLETION_CHECK_INTERVAL: 100, // Check completion every N ms + }; + + // Network and timing + static readonly NETWORK_CONFIG = { + FIREFOX_COMPATIBILITY_DELAY: 10, // ms delay for Firefox compatibility + FINALIZATION_TIMEOUT: 30000, // 30s timeout for file finalization + GRACEFUL_SHUTDOWN_TIMEOUT: 5000, // 5s timeout for graceful shutdown + }; + + // Validation thresholds + static readonly VALIDATION_CONFIG = { + MAX_SIZE_DIFFERENCE_BYTES: 1024, // Allow up to 1KB size difference for validation + MIN_PACKET_SIZE: 4, // Minimum embedded packet size (4 bytes for length header) + }; + + /** + * Get chunk index from file offset + */ + static getChunkIndexFromOffset(offset: number): number { + return Math.floor(offset / this.FILE_CONFIG.CHUNK_SIZE); + } + + /** + * Get file offset from chunk index + */ + static getOffsetFromChunkIndex(chunkIndex: number): number { + return chunkIndex * this.FILE_CONFIG.CHUNK_SIZE; + } + + /** + * Calculate expected chunks count for file size and offset + */ + static calculateExpectedChunks(fileSize: number, startOffset: number = 0): number { + return Math.ceil((fileSize - startOffset) / this.FILE_CONFIG.CHUNK_SIZE); + } + + /** + * Calculate total chunks in file + */ + static calculateTotalChunks(fileSize: number): number { + return Math.ceil(fileSize / this.FILE_CONFIG.CHUNK_SIZE); + } + + /** + * Check if file should be saved to disk + */ + static shouldSaveToDisk(fileSize: number, hasSaveDirectory: boolean): boolean { + return hasSaveDirectory || fileSize >= this.FILE_CONFIG.LARGE_FILE_THRESHOLD; + } +} \ No newline at end of file diff --git a/frontend/lib/receive/ReceptionStateManager.ts b/frontend/lib/receive/ReceptionStateManager.ts new file mode 100644 index 0000000..cef959c --- /dev/null +++ b/frontend/lib/receive/ReceptionStateManager.ts @@ -0,0 +1,358 @@ +import { + fileMetadata, + FolderProgress, + CurrentString, + CustomFile, +} from "@/types/webrtc"; + +/** + * 🚀 Active file reception state interface + */ +export interface ActiveFileReception { + meta: fileMetadata; + chunks: (ArrayBuffer | null)[]; + receivedSize: number; + initialOffset: number; + fileHandle: FileSystemFileHandle | null; + writeStream: FileSystemWritableFileStream | null; + sequencedWriter: any | null; // Will be typed properly when StreamingFileWriter is implemented + completionNotifier: { + resolve: () => void; + reject: (reason?: any) => void; + }; + receivedChunksCount: number; + expectedChunksCount: number; + chunkSequenceMap: Map; + isFinalized?: boolean; +} + +/** + * 🚀 Reception state management + * Centrally manages all file reception state data + */ +export class ReceptionStateManager { + // File metadata management + private pendingFilesMeta = new Map(); + + // Folder progress tracking + private folderProgresses: Record = {}; + + // Save type configuration (fileId/folderName -> isSavedToDisk) + public saveType: Record = {}; + + // Active transfer states + private activeFileReception: ActiveFileReception | null = null; + private activeStringReception: CurrentString | null = null; + private currentFolderName: string | null = null; + + // Peer information + private currentPeerId: string = ""; + private saveDirectory: FileSystemDirectoryHandle | null = null; + + // ===== File Metadata Management ===== + + /** + * Add file metadata + */ + public addFileMetadata(metadata: fileMetadata): boolean { + if (this.pendingFilesMeta.has(metadata.fileId)) { + return false; // Already exists + } + + this.pendingFilesMeta.set(metadata.fileId, metadata); + + // Update folder progress if this file belongs to a folder + if (metadata.folderName) { + this.addFileToFolder(metadata.folderName, metadata.fileId, metadata.size); + } + + return true; // New metadata added + } + + /** + * Get file metadata by ID + */ + public getFileMetadata(fileId: string): fileMetadata | undefined { + return this.pendingFilesMeta.get(fileId); + } + + /** + * Get all pending file metadata + */ + public getAllFileMetadata(): Map { + return new Map(this.pendingFilesMeta); + } + + /** + * Remove file metadata + */ + public removeFileMetadata(fileId: string): void { + this.pendingFilesMeta.delete(fileId); + } + + // ===== Folder Progress Management ===== + + /** + * Add file to folder progress tracking + */ + private addFileToFolder(folderName: string, fileId: string, fileSize: number): void { + if (!this.folderProgresses[folderName]) { + this.folderProgresses[folderName] = { + totalSize: 0, + receivedSize: 0, + fileIds: [], + }; + } + + const folderProgress = this.folderProgresses[folderName]; + if (!folderProgress.fileIds.includes(fileId)) { + folderProgress.fileIds.push(fileId); + folderProgress.totalSize += fileSize; + } + } + + /** + * Get folder progress + */ + public getFolderProgress(folderName: string): FolderProgress | undefined { + return this.folderProgresses[folderName]; + } + + /** + * Update folder received size + */ + public updateFolderReceivedSize(folderName: string, additionalBytes: number): void { + const folderProgress = this.folderProgresses[folderName]; + if (folderProgress) { + folderProgress.receivedSize += additionalBytes; + } + } + + /** + * Set folder received size (for resume scenarios) + */ + public setFolderReceivedSize(folderName: string, totalReceivedSize: number): void { + const folderProgress = this.folderProgresses[folderName]; + if (folderProgress) { + folderProgress.receivedSize = totalReceivedSize; + } + } + + /** + * Get all folder progresses + */ + public getAllFolderProgresses(): Record { + return { ...this.folderProgresses }; + } + + // ===== Active File Reception Management ===== + + /** + * Start active file reception + */ + public startFileReception( + meta: fileMetadata, + expectedChunksCount: number, + initialOffset: number = 0 + ): Promise { + if (this.activeFileReception) { + throw new Error("Another file reception is already in progress"); + } + + return new Promise((resolve, reject) => { + this.activeFileReception = { + meta, + chunks: new Array(expectedChunksCount).fill(null), + receivedSize: 0, + initialOffset, + fileHandle: null, + writeStream: null, + sequencedWriter: null, + completionNotifier: { resolve, reject }, + receivedChunksCount: 0, + expectedChunksCount, + chunkSequenceMap: new Map(), + isFinalized: false, + }; + }); + } + + /** + * Get active file reception + */ + public getActiveFileReception(): ActiveFileReception | null { + return this.activeFileReception; + } + + /** + * Update active file reception + */ + public updateActiveFileReception(updates: Partial): void { + if (this.activeFileReception) { + Object.assign(this.activeFileReception, updates); + } + } + + /** + * Complete active file reception + */ + public completeFileReception(): void { + if (this.activeFileReception?.completionNotifier) { + this.activeFileReception.completionNotifier.resolve(); + } + this.activeFileReception = null; + } + + /** + * Fail active file reception + */ + public failFileReception(reason: any): void { + if (this.activeFileReception?.completionNotifier) { + this.activeFileReception.completionNotifier.reject(reason); + } + this.activeFileReception = null; + } + + // ===== String Reception Management ===== + + /** + * Start string reception + */ + public startStringReception(length: number): void { + this.activeStringReception = { + length, + chunks: [], + receivedChunks: 0, + }; + } + + /** + * Get active string reception + */ + public getActiveStringReception(): CurrentString | null { + return this.activeStringReception; + } + + /** + * Update string reception chunk + */ + public updateStringReceptionChunk(index: number, chunk: string): void { + if (this.activeStringReception) { + this.activeStringReception.chunks[index] = chunk; + this.activeStringReception.receivedChunks++; + } + } + + /** + * Complete string reception + */ + public completeStringReception(): string | null { + if (!this.activeStringReception) return null; + + const fullString = this.activeStringReception.chunks.join(""); + this.activeStringReception = null; + return fullString; + } + + // ===== Current Context Management ===== + + /** + * Set current folder name + */ + public setCurrentFolderName(folderName: string | null): void { + this.currentFolderName = folderName; + } + + /** + * Get current folder name + */ + public getCurrentFolderName(): string | null { + return this.currentFolderName; + } + + /** + * Set current peer ID + */ + public setCurrentPeerId(peerId: string): void { + this.currentPeerId = peerId; + } + + /** + * Get current peer ID + */ + public getCurrentPeerId(): string { + return this.currentPeerId; + } + + /** + * Set save directory + */ + public setSaveDirectory(directory: FileSystemDirectoryHandle | null): void { + this.saveDirectory = directory; + } + + /** + * Get save directory + */ + public getSaveDirectory(): FileSystemDirectoryHandle | null { + return this.saveDirectory; + } + + // ===== Save Type Management ===== + + /** + * Set save type for file or folder + */ + public setSaveType(id: string, saveToDisk: boolean): void { + this.saveType[id] = saveToDisk; + } + + /** + * Get save type for file or folder + */ + public getSaveType(id: string): boolean { + return this.saveType[id] || false; + } + + // ===== State Reset and Cleanup ===== + + /** + * Force reset all states (for reconnection scenarios) + */ + public forceReset(): void { + this.pendingFilesMeta.clear(); + this.folderProgresses = {}; + this.saveType = {}; + this.activeFileReception = null; + this.activeStringReception = null; + this.currentFolderName = null; + this.currentPeerId = ""; + // Note: saveDirectory is preserved + } + + /** + * Graceful cleanup (preserve some state for potential resume) + */ + public gracefulCleanup(): void { + this.activeFileReception = null; + this.activeStringReception = null; + this.currentFolderName = null; + // Note: preserve pendingFilesMeta, folderProgresses, saveType for potential resume + } + + /** + * Get state statistics (for debugging) + */ + public getStateStats() { + return { + pendingFilesCount: this.pendingFilesMeta.size, + folderCount: Object.keys(this.folderProgresses).length, + hasActiveFileReception: !!this.activeFileReception, + hasActiveStringReception: !!this.activeStringReception, + currentFolderName: this.currentFolderName, + currentPeerId: this.currentPeerId, + hasSaveDirectory: !!this.saveDirectory, + saveTypeCount: Object.keys(this.saveType).length, + }; + } +} \ No newline at end of file diff --git a/frontend/lib/receive/StreamingFileWriter.ts b/frontend/lib/receive/StreamingFileWriter.ts new file mode 100644 index 0000000..00e9940 --- /dev/null +++ b/frontend/lib/receive/StreamingFileWriter.ts @@ -0,0 +1,398 @@ +import { ReceptionConfig } from "./ReceptionConfig"; +import { postLogToBackend } from "@/app/config/api"; + +const developmentEnv = process.env.NODE_ENV; + +/** + * 🚀 Strict Sequential Buffering Writer - Optimizes large file disk I/O performance + */ +export class SequencedDiskWriter { + private writeQueue = new Map(); + private nextWriteIndex = 0; + private readonly maxBufferSize: number; + private readonly stream: FileSystemWritableFileStream; + private totalWritten = 0; + + constructor(stream: FileSystemWritableFileStream, startIndex: number = 0) { + this.stream = stream; + this.nextWriteIndex = startIndex; + this.maxBufferSize = ReceptionConfig.BUFFER_CONFIG.MAX_BUFFER_SIZE; + } + + /** + * Write a chunk, automatically managing order and buffering + */ + async writeChunk(chunkIndex: number, chunk: ArrayBuffer): Promise { + // Debug writeChunk calls + if ( + ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING && + (chunkIndex <= 5 || chunkIndex === this.nextWriteIndex) + ) { + postLogToBackend( + `[DEBUG-RESUME] 🎯 WriteChunk called - received:${chunkIndex}, expected:${ + this.nextWriteIndex + }, match:${chunkIndex === this.nextWriteIndex}` + ); + } + + // 1. If it is the expected next chunk, write immediately + if (chunkIndex === this.nextWriteIndex) { + await this.flushSequentialChunks(chunk); + return; + } + + // 2. If it's a future chunk, buffer it + if (chunkIndex > this.nextWriteIndex) { + if (this.writeQueue.size < this.maxBufferSize) { + this.writeQueue.set(chunkIndex, chunk); + } else { + // Buffer full, forcing processing of the earliest chunk to free up space + await this.forceFlushOldest(); + this.writeQueue.set(chunkIndex, chunk); + } + return; + } + + // 3. If the chunk is expired, log a warning but ignore (already written) + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG] ⚠️ DUPLICATE chunk #${chunkIndex} ignored (already written #${this.nextWriteIndex})` + ); + } + } + + /** + * Write current chunk and attempt to sequentially write subsequent chunks + */ + private async flushSequentialChunks(firstChunk: ArrayBuffer): Promise { + let flushCount = 0; + + try { + // Write current chunk + await this.stream.write(firstChunk); + this.totalWritten += firstChunk.byteLength; + + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG] ✓ DISK_WRITE chunk #${this.nextWriteIndex} - ${firstChunk.byteLength} bytes, total: ${this.totalWritten}` + ); + } + + this.nextWriteIndex++; + + // Try to sequentially write chunks from buffer + while (this.writeQueue.has(this.nextWriteIndex)) { + const chunk = this.writeQueue.get(this.nextWriteIndex)!; + await this.stream.write(chunk); + this.totalWritten += chunk.byteLength; + this.writeQueue.delete(this.nextWriteIndex); + + flushCount++; + this.nextWriteIndex++; + } + } catch (error) { + // Defensive handling: If stream is closed, silently ignore + const errorMessage = + error instanceof Error ? error.message : String(error); + if ( + errorMessage.includes("closing writable stream") || + errorMessage.includes("stream is closed") + ) { + console.log( + `[SequencedDiskWriter] Stream closed during write - ignoring remaining chunks` + ); + return; + } + // Re-throw other types of errors + throw error; + } + + if (flushCount > 0 && ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG] 🔥 SEQUENTIAL_FLUSH ${flushCount} chunks, now at #${this.nextWriteIndex}, queue: ${this.writeQueue.size}` + ); + } + } + + /** + * Get the next expected write index + */ + get expectedIndex(): number { + return this.nextWriteIndex; + } + + /** + * Force flush the earliest chunk to release buffer space + */ + private async forceFlushOldest(): Promise { + try { + if (this.writeQueue.size === 0) return; + + const oldestIndex = Math.min(...Array.from(this.writeQueue.keys())); + const chunk = this.writeQueue.get(oldestIndex)!; + + // Use seek to write at the correct position (fallback handling) + const fileOffset = ReceptionConfig.getOffsetFromChunkIndex(oldestIndex); + await this.stream.seek(fileOffset); + await this.stream.write(chunk); + this.writeQueue.delete(oldestIndex); + + // Return to current position + const currentOffset = ReceptionConfig.getOffsetFromChunkIndex(this.nextWriteIndex); + await this.stream.seek(currentOffset); + } catch (error) { + // Defensive handling: If stream is closed, silently ignore + const errorMessage = + error instanceof Error ? error.message : String(error); + if ( + errorMessage.includes("closing writable stream") || + errorMessage.includes("stream is closed") + ) { + console.log( + `[SequencedDiskWriter] Stream closed during write - ignoring remaining chunks` + ); + return; + } + // Re-throw other types of errors + throw error; + } + } + + /** + * Get buffer status + */ + getBufferStatus(): { + queueSize: number; + nextIndex: number; + totalWritten: number; + } { + return { + queueSize: this.writeQueue.size, + nextIndex: this.nextWriteIndex, + totalWritten: this.totalWritten, + }; + } + + /** + * Close and clean up resources + */ + async close(): Promise { + try { + // Try to flush all remaining chunks + const remainingIndexes = Array.from(this.writeQueue.keys()).sort( + (a, b) => a - b + ); + for (const chunkIndex of remainingIndexes) { + const chunk = this.writeQueue.get(chunkIndex)!; + const fileOffset = ReceptionConfig.getOffsetFromChunkIndex(chunkIndex); + await this.stream.seek(fileOffset); + await this.stream.write(chunk); + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG] 💾 FINAL_FLUSH chunk #${chunkIndex} at cleanup` + ); + } + } + } catch (error) { + // Defensive handling: If stream is not writable during close, handle silently + const errorMessage = + error instanceof Error ? error.message : String(error); + if ( + errorMessage.includes("closing writable stream") || + errorMessage.includes("stream is closed") + ) { + console.log( + `[SequencedDiskWriter] Stream closed during final flush - data may be incomplete` + ); + } else { + console.warn( + `[SequencedDiskWriter] Error during final flush:`, + errorMessage + ); + throw error; + } + } + + this.writeQueue.clear(); + } +} + +/** + * 🚀 Streaming file writer + * Manages disk file creation, directory structure, and streaming writes + */ +export class StreamingFileWriter { + private saveDirectory: FileSystemDirectoryHandle | null = null; + + constructor(saveDirectory?: FileSystemDirectoryHandle) { + this.saveDirectory = saveDirectory || null; + } + + /** + * Set save directory + */ + setSaveDirectory(directory: FileSystemDirectoryHandle): void { + this.saveDirectory = directory; + } + + /** + * Create disk write stream for a file + */ + async createWriteStream( + fileName: string, + fullPath: string, + offset: number = 0 + ): Promise<{ + fileHandle: FileSystemFileHandle; + writeStream: FileSystemWritableFileStream; + sequencedWriter: SequencedDiskWriter; + }> { + if (!this.saveDirectory) { + throw new Error("Save directory not set"); + } + + try { + const folderHandle = await this.createFolderStructure(fullPath); + const fileHandle = await folderHandle.getFileHandle(fileName, { + create: true, + }); + + // Use keepExistingData: true to append + const writeStream = await fileHandle.createWritable({ + keepExistingData: true, + }); + + // Seek to the offset to start writing from there + await writeStream.seek(offset); + + // Create strictly sequential write manager + const startChunkIndex = ReceptionConfig.getChunkIndexFromOffset(offset); + const sequencedWriter = new SequencedDiskWriter(writeStream, startChunkIndex); + + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG] 📢 SEQUENCED_WRITER created - startIndex: ${startChunkIndex}, offset: ${offset}` + ); + postLogToBackend( + `[DEBUG-RESUME] 🎯 SequencedWriter expects - startIndex:${startChunkIndex}, offset:${offset}, calculatedFrom:${offset}/65536` + ); + } + + return { fileHandle, writeStream, sequencedWriter }; + } catch (err) { + throw new Error(`Failed to create file on disk: ${err}`); + } + } + + /** + * Check if partial file exists and get its size + */ + async getPartialFileSize(fileName: string, fullPath: string): Promise { + if (!this.saveDirectory) { + return 0; + } + + try { + const folderHandle = await this.createFolderStructure(fullPath); + const fileHandle = await folderHandle.getFileHandle(fileName, { + create: false, + }); + const file = await fileHandle.getFile(); + return file.size; + } catch { + // File does not exist + return 0; + } + } + + /** + * Create folder structure based on full path + */ + private async createFolderStructure( + fullPath: string + ): Promise { + if (!this.saveDirectory) { + throw new Error("Save directory not set"); + } + + const parts = fullPath.split("/"); + parts.pop(); // Remove filename + + let currentDir = this.saveDirectory; + for (const part of parts) { + if (part) { + currentDir = await currentDir.getDirectoryHandle(part, { + create: true, + }); + } + } + return currentDir; + } + + /** + * Finalize file write and close streams + */ + async finalizeWrite( + sequencedWriter: SequencedDiskWriter, + writeStream: FileSystemWritableFileStream, + fileName: string + ): Promise { + try { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG-FINALIZE] 🚀 Starting finalization for ${fileName}` + ); + } + + // First close the strict sequential writing manager (flush all buffers) + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend(`[DEBUG-FINALIZE] Closing SequencedWriter...`); + } + await sequencedWriter.close(); + const status = sequencedWriter.getBufferStatus(); + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG-FINALIZE] 💾 SEQUENCED_WRITER closed - totalWritten: ${status.totalWritten}, finalQueue: ${status.queueSize}` + ); + } + + // Then close the file stream + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG-FINALIZE] About to close writeStream for ${fileName}` + ); + } + await writeStream.close(); + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend(`[DEBUG-FINALIZE] ✅ WriteStream closed successfully`); + } + + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG-FINALIZE] ✅ LARGE_FILE finalized successfully - ${fileName}` + ); + } + } catch (error) { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG-FINALIZE] ❌ Error during finalization: ${error}` + ); + } + throw new Error(`Error finalizing large file: ${error}`); + } + } + + /** + * Check if save directory is available + */ + hasSaveDirectory(): boolean { + return !!this.saveDirectory; + } + + /** + * Get save directory + */ + getSaveDirectory(): FileSystemDirectoryHandle | null { + return this.saveDirectory; + } +} \ No newline at end of file diff --git a/frontend/lib/receive/index.ts b/frontend/lib/receive/index.ts new file mode 100644 index 0000000..dbe3315 --- /dev/null +++ b/frontend/lib/receive/index.ts @@ -0,0 +1,45 @@ +/** + * 🚀 File receive module unified export + * Provides modular file reception services + */ + +// Configuration management +export { ReceptionConfig } from "./ReceptionConfig"; + +// State management +export { ReceptionStateManager } from "./ReceptionStateManager"; +export type { ActiveFileReception } from "./ReceptionStateManager"; + +// Data processing +export { ChunkProcessor } from "./ChunkProcessor"; +export type { ChunkProcessingResult } from "./ChunkProcessor"; + +// File writing +export { StreamingFileWriter, SequencedDiskWriter } from "./StreamingFileWriter"; + +// File assembly +export { FileAssembler } from "./FileAssembler"; +export type { FileAssemblyResult } from "./FileAssembler"; + +// Message processing +export { MessageProcessor } from "./MessageProcessor"; +export type { MessageProcessorDelegate } from "./MessageProcessor"; + +// Progress reporting +export { ProgressReporter } from "./ProgressReporter"; +export type { ProgressCallback, ProgressStats } from "./ProgressReporter"; + +// Main orchestrator +export { FileReceiveOrchestrator } from "./FileReceiveOrchestrator"; + +/** + * 🎯 Convenience creation function - Quick initialization of file receive services + */ +import WebRTC_Recipient from "../webrtc_Recipient"; +import { FileReceiveOrchestrator } from "./FileReceiveOrchestrator"; + +export function createFileReceiveService( + webrtcConnection: WebRTC_Recipient +): FileReceiveOrchestrator { + return new FileReceiveOrchestrator(webrtcConnection); +} \ No newline at end of file From 327de90f52e4ebabd4493b714fb3e43ac45b10f4 Mon Sep 17 00:00:00 2001 From: david_bai Date: Sun, 14 Sep 2025 11:29:51 +0800 Subject: [PATCH 04/10] chore:Fix the issue where the breakpoint resume receiver is missing one chunk of data --- frontend/lib/receive/ChunkProcessor.ts | 62 +++++------- .../lib/receive/FileReceiveOrchestrator.ts | 19 ++-- frontend/lib/transfer/StreamingFileReader.ts | 99 ++++++++----------- frontend/lib/utils/ChunkRangeCalculator.ts | 82 +++++++++++++++ 4 files changed, 153 insertions(+), 109 deletions(-) create mode 100644 frontend/lib/utils/ChunkRangeCalculator.ts diff --git a/frontend/lib/receive/ChunkProcessor.ts b/frontend/lib/receive/ChunkProcessor.ts index 0c6b846..604def3 100644 --- a/frontend/lib/receive/ChunkProcessor.ts +++ b/frontend/lib/receive/ChunkProcessor.ts @@ -148,9 +148,10 @@ export class ChunkProcessor { const startChunkIndex = ReceptionConfig.getChunkIndexFromOffset(initialOffset); // Resume start index const relativeChunkIndex = absoluteChunkIndex - startChunkIndex; // Relative index in chunks array - if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING && absoluteChunkIndex <= 970) { + // 🎯 简化调试:只在边界chunk时记录索引映射 + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING && (absoluteChunkIndex <= 2 || relativeChunkIndex <= 2)) { postLogToBackend( - `[DEBUG-CHUNKS] Index mapping - absolute:${absoluteChunkIndex}, start:${startChunkIndex}, relative:${relativeChunkIndex}` + `[INDEX-MAP] abs:${absoluteChunkIndex}, start:${startChunkIndex}, rel:${relativeChunkIndex}` ); } @@ -195,10 +196,11 @@ export class ChunkProcessor { const startChunkIndex = ReceptionConfig.getChunkIndexFromOffset(initialOffset); const calculatedExpected = chunkMeta.totalChunks - startChunkIndex; + // 🎯 简化日志:只在数量不匹配时记录关键信息 if (chunkMeta.totalChunks !== expectedChunksCount && calculatedExpected !== expectedChunksCount) { if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { postLogToBackend( - `[DEBUG-CHUNKS] Chunk count info - fileTotal: ${chunkMeta.totalChunks}, currentExpected: ${expectedChunksCount}, calculatedExpected: ${calculatedExpected}, startChunk: ${startChunkIndex}` + `[CHUNK-COUNT-MISMATCH] fileTotal:${chunkMeta.totalChunks}, expected:${expectedChunksCount}, calculated:${calculatedExpected}` ); } } @@ -231,14 +233,17 @@ export class ChunkProcessor { return; } + // 🎯 简化日志:只记录边界chunk和异常情况 const { chunkMeta, absoluteChunkIndex, relativeChunkIndex } = result; - const lastFewChunks = relativeChunkIndex >= expectedChunksCount - 5; + const isFirstFew = absoluteChunkIndex <= 3; + const isLastFew = relativeChunkIndex >= expectedChunksCount - 3; + const hasIndexMismatch = writerExpectedIndex !== undefined && relativeChunkIndex !== writerExpectedIndex; - if (absoluteChunkIndex <= 970 || lastFewChunks) { + if (isFirstFew || isLastFew || hasIndexMismatch) { postLogToBackend( - `[DEBUG-CHUNKS] 📦 Chunk #${absoluteChunkIndex} received - relative:${relativeChunkIndex}, size:${chunkMeta.chunkSize}${ - writerExpectedIndex !== undefined ? `, writerExpects:${writerExpectedIndex}` : '' - }, isLastFew:${lastFewChunks}` + `[CHUNK-DETAIL] #${absoluteChunkIndex} rel:${relativeChunkIndex}${ + hasIndexMismatch ? ` MISMATCH(expected:${writerExpectedIndex})` : '' + } size:${chunkMeta.chunkSize}` ); } } @@ -304,39 +309,20 @@ export class ChunkProcessor { const { sequencedCount, expectedChunksCount, currentTotalSize, expectedSize, isDataComplete } = stats; - // Only log at key moments to reduce noise - if ( - isDataComplete || - sequencedCount % ReceptionConfig.DEBUG_CONFIG.PROGRESS_LOG_INTERVAL === 0 || - sequencedCount > expectedChunksCount - 10 - ) { - // Check last few chunks status - const lastChunkIndex = expectedChunksCount - 1; - const lastFewChunks = []; + // 🎯 关键日志3:只在完成时打印最终检查结果 + if (isDataComplete) { const startChunkIndex = ReceptionConfig.getChunkIndexFromOffset(initialOffset); - - for (let i = Math.max(0, lastChunkIndex - 3); i <= lastChunkIndex; i++) { - const chunk = chunks[i]; - const exists = chunk instanceof ArrayBuffer; - const size = exists ? (chunk as ArrayBuffer).byteLength : 0; - const absoluteIndex = startChunkIndex + i; - lastFewChunks.push(`rel#${i}(abs#${absoluteIndex}):${exists}(${size})`); + const missingChunks = []; + + for (let i = 0; i < expectedChunksCount; i++) { + if (!chunks[i]) { + const absoluteIndex = startChunkIndex + i; + missingChunks.push(absoluteIndex); + } } - - postLogToBackend(`[DEBUG-COMPLETE] Check completion - file:${fileName}`); + postLogToBackend( - `[DEBUG-COMPLETE] Chunks: received:${sequencedCount}/${expectedChunksCount}, isSequenceComplete:${stats.sequencedCount === expectedChunksCount}` - ); - postLogToBackend( - `[DEBUG-COMPLETE] Size: current:${currentTotalSize}, expected:${expectedSize}, sizeComplete:${currentTotalSize >= expectedSize}, diff:${ - expectedSize - currentTotalSize - }` - ); - postLogToBackend( - `[DEBUG-COMPLETE] LastChunks: ${lastFewChunks.join(", ")}` - ); - postLogToBackend( - `[DEBUG-COMPLETE] IsDataComplete: ${isDataComplete}` + `[FINAL-CHECK] File: ${fileName}, received: ${sequencedCount}/${expectedChunksCount}, sizeDiff: ${expectedSize - currentTotalSize}, missing: [${missingChunks.join(',')}]` ); } } diff --git a/frontend/lib/receive/FileReceiveOrchestrator.ts b/frontend/lib/receive/FileReceiveOrchestrator.ts index 93c25d0..ade17bc 100644 --- a/frontend/lib/receive/FileReceiveOrchestrator.ts +++ b/frontend/lib/receive/FileReceiveOrchestrator.ts @@ -7,6 +7,7 @@ import { StreamingFileWriter, SequencedDiskWriter } from "./StreamingFileWriter" import { FileAssembler } from "./FileAssembler"; import { ProgressReporter, ProgressCallback } from "./ProgressReporter"; import { ReceptionConfig } from "./ReceptionConfig"; +import { ChunkRangeCalculator } from "@/lib/utils/ChunkRangeCalculator"; import { postLogToBackend } from "@/app/config/api"; const developmentEnv = process.env.NODE_ENV; @@ -135,19 +136,15 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { ); if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { - const totalChunks = ReceptionConfig.calculateTotalChunks(fileInfo.size); - const startChunkIndex = ReceptionConfig.getChunkIndexFromOffset(offset); - - postLogToBackend(`[DEBUG-CHUNKS] File: ${fileInfo.name}`); - postLogToBackend( - `[DEBUG-CHUNKS] File size: ${fileInfo.size}, offset: ${offset}` + // 🎯 关键日志2:接收端总结信息 - 使用统一的chunk范围计算逻辑 + const chunkRange = ChunkRangeCalculator.getChunkRange( + fileInfo.size, + offset, + ReceptionConfig.FILE_CONFIG.CHUNK_SIZE ); + postLogToBackend( - `[DEBUG-CHUNKS] Total chunks in file: ${totalChunks} (0-${totalChunks - 1})` - ); - postLogToBackend(`[DEBUG-CHUNKS] Start chunk index: ${startChunkIndex}`); - postLogToBackend( - `[DEBUG-CHUNKS] Expected chunks: ${expectedChunksCount}` + `[RECV-SUMMARY] File: ${fileInfo.name}, expected: ${expectedChunksCount}, calculated: ${chunkRange.totalChunks}, startChunk: ${chunkRange.startChunk}, endChunk: ${chunkRange.endChunk}, absoluteTotal: ${chunkRange.absoluteTotalChunks}` ); } diff --git a/frontend/lib/transfer/StreamingFileReader.ts b/frontend/lib/transfer/StreamingFileReader.ts index 3a22e71..d500dae 100644 --- a/frontend/lib/transfer/StreamingFileReader.ts +++ b/frontend/lib/transfer/StreamingFileReader.ts @@ -1,5 +1,6 @@ import { CustomFile } from "@/types/webrtc"; import { TransferConfig } from "./TransferConfig"; +import { ChunkRangeCalculator } from "@/lib/utils/ChunkRangeCalculator"; import { postLogToBackend } from "@/app/config/api"; const developmentEnv = process.env.NODE_ENV; /** @@ -51,19 +52,14 @@ export class StreamingFileReader { 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 + // 🎯 关键日志1:发送端总结信息 - 使用统一的chunk范围计算逻辑 + const chunkRange = ChunkRangeCalculator.getChunkRange( + this.totalFileSize, + startOffset, + this.NETWORK_CHUNK_SIZE ); postLogToBackend( - `[DEBUG-RESUME] 🏗️ StreamingFileReader created - totalFileOffset:${this.totalFileOffset}, currentBatchStartOffset:${this.currentBatchStartOffset}, expectedGlobalChunk:${expectedGlobalChunk}` + `[SEND-SUMMARY] File: ${file.name}, offset: ${startOffset}, startChunk: ${chunkRange.startChunk}, endChunk: ${chunkRange.endChunk}, willSend: ${chunkRange.totalChunks}, absoluteTotal: ${chunkRange.absoluteTotalChunks}` ); } } @@ -96,15 +92,22 @@ export class StreamingFileReader { // 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}` + // 🎯 关键日志:边界chunk验证(临时保留用于验证修复效果) + if (developmentEnv === "development") { + const totalChunks = this.calculateTotalNetworkChunks(); + const currentOffset = this.totalFileOffset - networkChunk.byteLength; + const firstChunkIndex = Math.floor( + currentOffset / this.NETWORK_CHUNK_SIZE ); + const isFirst = + globalChunkIndex === firstChunkIndex || + (currentOffset === 0 && globalChunkIndex === 0); + + if (isFirst || isLast) { + postLogToBackend( + `[BOUNDARY] Chunk #${globalChunkIndex}/${totalChunks}, isFirst: ${isFirst}, isLast: ${isLast}, size: ${networkChunk.byteLength}` + ); + } } return { @@ -181,35 +184,24 @@ export class StreamingFileReader { 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; - } + // 🔧 修复:简化batch内索引计算逻辑 + // 由于calculateGlobalChunkIndex现在直接基于totalFileOffset计算, + // batch内索引只需要基于当前batch的起始位置计算即可 + const chunkOffsetInBatch = + batchStartOffset - + Math.floor(batchStartOffset / this.BATCH_SIZE) * this.BATCH_SIZE; + this.currentChunkIndexInBatch = Math.floor( + chunkOffsetInBatch / this.NETWORK_CHUNK_SIZE + ); - // Only output batch reading logs in development environment - if (developmentEnv === "development") { + // Only output essential batch reading logs in development environment + if (developmentEnv === "development" && batchSize > this.BATCH_SIZE / 2) { const totalTime = performance.now() - startTime; const speedMBps = batchSize / 1024 / 1024 / (totalTime / 1000); postLogToBackend( - `[DEBUG] 📖 BATCH_READ - size: ${(batchSize / 1024 / 1024).toFixed( + `[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}` + )}MB, speed: ${speedMBps.toFixed(1)}MB/s` ); } } catch (error) { @@ -278,25 +270,12 @@ export class StreamingFileReader { /** * 📊 Calculate global network chunk index + * 🔧 Simplified logic: directly calculate based on file offset to avoid batch boundary errors */ 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; + // 🎯 核心修复:直接基于当前文件偏移量计算chunk索引,避免复杂的batch计算 + // 这确保了与接收端ReceptionConfig.getChunkIndexFromOffset()完全一致的计算逻辑 + return Math.floor(this.totalFileOffset / this.NETWORK_CHUNK_SIZE); } /** diff --git a/frontend/lib/utils/ChunkRangeCalculator.ts b/frontend/lib/utils/ChunkRangeCalculator.ts new file mode 100644 index 0000000..af4e331 --- /dev/null +++ b/frontend/lib/utils/ChunkRangeCalculator.ts @@ -0,0 +1,82 @@ +/** + * 🚀 Chunk range calculation utilities + * Provides unified chunk calculation logic to ensure consistency between sender and receiver + */ + +export class ChunkRangeCalculator { + /** + * Calculate chunk range for a file with given parameters + * This method ensures both sender and receiver use identical calculation logic + */ + static getChunkRange(fileSize: number, startOffset: number, chunkSize: number) { + // Calculate starting chunk index + const startChunk = Math.floor(startOffset / chunkSize); + + // Calculate ending chunk index based on the last byte of the file + const lastByteIndex = fileSize - 1; + const endChunk = Math.floor(lastByteIndex / chunkSize); + + // Calculate total chunks to be sent/received (from startChunk to endChunk inclusive) + const totalChunks = endChunk - startChunk + 1; + + // Calculate absolute total chunks in the entire file + const absoluteTotalChunks = Math.ceil(fileSize / chunkSize); + + return { + startChunk, // First chunk index to process + endChunk, // Last chunk index to process + totalChunks, // Number of chunks to process (for resume transfers) + absoluteTotalChunks // Total chunks in the entire file + }; + } + + /** + * Calculate expected chunks count for resume transfer + * Identical to ReceptionConfig.calculateExpectedChunks() + */ + static calculateExpectedChunks(fileSize: number, startOffset: number, chunkSize: number): number { + return Math.ceil((fileSize - startOffset) / chunkSize); + } + + /** + * Get chunk index from file offset + * Identical to ReceptionConfig.getChunkIndexFromOffset() + */ + static getChunkIndexFromOffset(offset: number, chunkSize: number): number { + return Math.floor(offset / chunkSize); + } + + /** + * Get file offset from chunk index + * Identical to ReceptionConfig.getOffsetFromChunkIndex() + */ + static getOffsetFromChunkIndex(chunkIndex: number, chunkSize: number): number { + return chunkIndex * chunkSize; + } + + /** + * Validate chunk index within expected range + */ + static isChunkIndexValid( + chunkIndex: number, + startOffset: number, + fileSize: number, + chunkSize: number + ): boolean { + const range = this.getChunkRange(fileSize, startOffset, chunkSize); + return chunkIndex >= range.startChunk && chunkIndex <= range.endChunk; + } + + /** + * Calculate relative chunk index from absolute chunk index + * Used by receiver to map sender's absolute index to local array index + */ + static getRelativeChunkIndex( + absoluteChunkIndex: number, + startOffset: number, + chunkSize: number + ): number { + const startChunkIndex = this.getChunkIndexFromOffset(startOffset, chunkSize); + return absoluteChunkIndex - startChunkIndex; + } +} From 4dcdf0c3a06521d34fe252e7269dcb8fba99c0d2 Mon Sep 17 00:00:00 2001 From: david_bai Date: Sun, 14 Sep 2025 11:44:35 +0800 Subject: [PATCH 05/10] chore:The breakpoint resuming file is saved normally --- frontend/lib/receive/ChunkProcessor.ts | 6 +- .../lib/receive/FileReceiveOrchestrator.ts | 2 + frontend/lib/receive/StreamingFileWriter.ts | 68 ++++++++++++++----- .../lib/transfer/FileTransferOrchestrator.ts | 16 +++-- frontend/lib/transfer/StreamingFileReader.ts | 20 +++--- 5 files changed, 76 insertions(+), 36 deletions(-) diff --git a/frontend/lib/receive/ChunkProcessor.ts b/frontend/lib/receive/ChunkProcessor.ts index 604def3..853fabc 100644 --- a/frontend/lib/receive/ChunkProcessor.ts +++ b/frontend/lib/receive/ChunkProcessor.ts @@ -237,12 +237,14 @@ export class ChunkProcessor { const { chunkMeta, absoluteChunkIndex, relativeChunkIndex } = result; const isFirstFew = absoluteChunkIndex <= 3; const isLastFew = relativeChunkIndex >= expectedChunksCount - 3; - const hasIndexMismatch = writerExpectedIndex !== undefined && relativeChunkIndex !== writerExpectedIndex; + + // 🔧 修复:SequencedWriter期望的是绝对索引,不是相对索引 + const hasIndexMismatch = writerExpectedIndex !== undefined && absoluteChunkIndex !== writerExpectedIndex; if (isFirstFew || isLastFew || hasIndexMismatch) { postLogToBackend( `[CHUNK-DETAIL] #${absoluteChunkIndex} rel:${relativeChunkIndex}${ - hasIndexMismatch ? ` MISMATCH(expected:${writerExpectedIndex})` : '' + hasIndexMismatch ? ` MISMATCH(writer expects:${writerExpectedIndex})` : '' } size:${chunkMeta.chunkSize}` ); } diff --git a/frontend/lib/receive/FileReceiveOrchestrator.ts b/frontend/lib/receive/FileReceiveOrchestrator.ts index ade17bc..1d98337 100644 --- a/frontend/lib/receive/FileReceiveOrchestrator.ts +++ b/frontend/lib/receive/FileReceiveOrchestrator.ts @@ -367,12 +367,14 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { // Handle disk writing if needed if (reception.sequencedWriter) { + // 🔧 修复:SequencedWriter使用绝对索引,确保传递正确的索引 this.chunkProcessor.logChunkDetails( result, reception.expectedChunksCount, reception.sequencedWriter.expectedIndex ); + // ✅ 正确使用绝对索引进行磁盘写入 await reception.sequencedWriter.writeChunk( result.absoluteChunkIndex, result.chunkData diff --git a/frontend/lib/receive/StreamingFileWriter.ts b/frontend/lib/receive/StreamingFileWriter.ts index 00e9940..9bfd62d 100644 --- a/frontend/lib/receive/StreamingFileWriter.ts +++ b/frontend/lib/receive/StreamingFileWriter.ts @@ -178,42 +178,74 @@ export class SequencedDiskWriter { */ async close(): Promise { try { - // Try to flush all remaining chunks + // 🔧 修复:确保以正确的WriteParams格式写入剩余chunks const remainingIndexes = Array.from(this.writeQueue.keys()).sort( (a, b) => a - b ); - for (const chunkIndex of remainingIndexes) { - const chunk = this.writeQueue.get(chunkIndex)!; - const fileOffset = ReceptionConfig.getOffsetFromChunkIndex(chunkIndex); - await this.stream.seek(fileOffset); - await this.stream.write(chunk); + + if (remainingIndexes.length > 0) { if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { postLogToBackend( - `[DEBUG] 💾 FINAL_FLUSH chunk #${chunkIndex} at cleanup` + `[DEBUG-FINALIZE] 💾 Flushing ${remainingIndexes.length} remaining chunks: [${remainingIndexes.join(',')}]` ); } + + for (const chunkIndex of remainingIndexes) { + const chunk = this.writeQueue.get(chunkIndex)!; + const fileOffset = ReceptionConfig.getOffsetFromChunkIndex(chunkIndex); + + // 🔧 修复:使用正确的WriteParams格式 + await this.stream.seek(fileOffset); + + // 确保chunk是有效的ArrayBuffer + if (!(chunk instanceof ArrayBuffer) || chunk.byteLength === 0) { + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG-FINALIZE] ⚠️ Skipping invalid chunk #${chunkIndex}: ${Object.prototype.toString.call(chunk)}, size: ${chunk.byteLength}` + ); + } + continue; + } + + // 使用标准WriteParams格式写入 + await this.stream.write({ + type: "write", + data: chunk + }); + + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG-FINALIZE] ✅ FINAL_FLUSH chunk #${chunkIndex} (${chunk.byteLength} bytes)` + ); + } + } } } catch (error) { - // Defensive handling: If stream is not writable during close, handle silently - const errorMessage = - error instanceof Error ? error.message : String(error); + // Enhanced error handling with specific error types + const errorMessage = error instanceof Error ? error.message : String(error); + + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG-FINALIZE] ❌ Error during final flush: ${errorMessage}` + ); + } + if ( errorMessage.includes("closing writable stream") || - errorMessage.includes("stream is closed") + errorMessage.includes("stream is closed") || + errorMessage.includes("The stream is not in a state that permits this operation") ) { console.log( - `[SequencedDiskWriter] Stream closed during final flush - data may be incomplete` + `[SequencedDiskWriter] Stream closed during final flush - completing gracefully` ); } else { - console.warn( - `[SequencedDiskWriter] Error during final flush:`, - errorMessage - ); + console.warn(`[SequencedDiskWriter] Unexpected error during final flush:`, errorMessage); throw error; } + } finally { + // 无论如何都要清理队列 + this.writeQueue.clear(); } - - this.writeQueue.clear(); } } diff --git a/frontend/lib/transfer/FileTransferOrchestrator.ts b/frontend/lib/transfer/FileTransferOrchestrator.ts index 842e4e5..832cfab 100644 --- a/frontend/lib/transfer/FileTransferOrchestrator.ts +++ b/frontend/lib/transfer/FileTransferOrchestrator.ts @@ -205,11 +205,11 @@ export class FileTransferOrchestrator implements MessageHandlerDelegate { const peerState = this.stateManager.getPeerState(peerId); const transferStartTime = performance.now(); + // 🔧 修复:记录传输开始时的初始offset,用于后续统计计算 + const initialReadOffset = peerState.readOffset || 0; + // 1. Initialize streaming file reader - const streamReader = new StreamingFileReader( - file, - peerState.readOffset || 0 - ); + const streamReader = new StreamingFileReader(file, initialReadOffset); if (developmentEnv === "development") { postLogToBackend( @@ -304,9 +304,11 @@ export class FileTransferOrchestrator implements MessageHandlerDelegate { if (developmentEnv === "development") { const totalTime = performance.now() - transferStartTime; const avgSpeedMBps = totalBytesSent / 1024 / 1024 / (totalTime / 1000); + + // 🔧 修复:使用正确的初始offset而不是当前readOffset来计算日志统计 + const initialOffset = initialReadOffset || 0; // 传输开始时的offset const expectedTotalChunks = Math.ceil(file.size / 65536); - const startOffset = peerState.readOffset || 0; - const startChunkIndex = Math.floor(startOffset / 65536); + const startChunkIndex = Math.floor(initialOffset / 65536); const expectedChunksSent = expectedTotalChunks - startChunkIndex; postLogToBackend( @@ -315,7 +317,7 @@ export class FileTransferOrchestrator implements MessageHandlerDelegate { ).toFixed(1)}s, speed: ${avgSpeedMBps.toFixed(1)}MB/s` ); postLogToBackend( - `[DEBUG-CHUNKS] Chunks sent: ${networkChunkIndex}, expected: ${expectedChunksSent}, startChunk: ${startChunkIndex}, totalFileChunks: ${expectedTotalChunks}` + `[DEBUG-CHUNKS] Chunks sent: ${networkChunkIndex}, expected: ${expectedChunksSent}, startChunk: ${startChunkIndex}, totalFileChunks: ${expectedTotalChunks}, initialOffset: ${initialOffset}` ); if (networkChunkIndex !== expectedChunksSent) { diff --git a/frontend/lib/transfer/StreamingFileReader.ts b/frontend/lib/transfer/StreamingFileReader.ts index d500dae..e015520 100644 --- a/frontend/lib/transfer/StreamingFileReader.ts +++ b/frontend/lib/transfer/StreamingFileReader.ts @@ -40,6 +40,7 @@ export class StreamingFileReader { // Global state private totalFileOffset = 0; // Current position in the entire file + private startChunkIndex = 0; // 🔧 记录传输起始的chunk索引 private isFinished = false; private isReading = false; // Prevent concurrent reading @@ -51,6 +52,9 @@ export class StreamingFileReader { this.currentBatchStartOffset = startOffset; this.fileReader = new FileReader(); + // 🔧 记录传输的起始chunk索引,用于边界检测 + this.startChunkIndex = Math.floor(startOffset / this.NETWORK_CHUNK_SIZE); + if (developmentEnv === "development") { // 🎯 关键日志1:发送端总结信息 - 使用统一的chunk范围计算逻辑 const chunkRange = ChunkRangeCalculator.getChunkRange( @@ -95,17 +99,15 @@ export class StreamingFileReader { // 🎯 关键日志:边界chunk验证(临时保留用于验证修复效果) if (developmentEnv === "development") { const totalChunks = this.calculateTotalNetworkChunks(); - const currentOffset = this.totalFileOffset - networkChunk.byteLength; - const firstChunkIndex = Math.floor( - currentOffset / this.NETWORK_CHUNK_SIZE - ); - const isFirst = - globalChunkIndex === firstChunkIndex || - (currentOffset === 0 && globalChunkIndex === 0); + + // 🔧 修复:使用简化的边界检测逻辑 + const isFirst = globalChunkIndex === this.startChunkIndex; + const expectedLastChunk = Math.floor((this.totalFileSize - 1) / this.NETWORK_CHUNK_SIZE); + const isRealLast = isLast && globalChunkIndex === expectedLastChunk; - if (isFirst || isLast) { + if (isFirst || isRealLast) { postLogToBackend( - `[BOUNDARY] Chunk #${globalChunkIndex}/${totalChunks}, isFirst: ${isFirst}, isLast: ${isLast}, size: ${networkChunk.byteLength}` + `[BOUNDARY] Chunk #${globalChunkIndex}/${totalChunks}, isFirst: ${isFirst}, isLast: ${isRealLast}, startIdx: ${this.startChunkIndex}, expectedLastIdx: ${expectedLastChunk}, size: ${networkChunk.byteLength}` ); } } From 79089bed7ecc7bb86026d7b1c618ae0a1a925bd8 Mon Sep 17 00:00:00 2001 From: david_bai Date: Sun, 14 Sep 2025 16:49:06 +0800 Subject: [PATCH 06/10] chore:Saving folders to disk now works correctly --- .../lib/receive/FileReceiveOrchestrator.ts | 121 +++++++++++++----- 1 file changed, 86 insertions(+), 35 deletions(-) diff --git a/frontend/lib/receive/FileReceiveOrchestrator.ts b/frontend/lib/receive/FileReceiveOrchestrator.ts index 1d98337..ffa4a34 100644 --- a/frontend/lib/receive/FileReceiveOrchestrator.ts +++ b/frontend/lib/receive/FileReceiveOrchestrator.ts @@ -3,7 +3,10 @@ import { CustomFile, fileMetadata } from "@/types/webrtc"; import { ReceptionStateManager } from "./ReceptionStateManager"; import { MessageProcessor, MessageProcessorDelegate } from "./MessageProcessor"; import { ChunkProcessor, ChunkProcessingResult } from "./ChunkProcessor"; -import { StreamingFileWriter, SequencedDiskWriter } from "./StreamingFileWriter"; +import { + StreamingFileWriter, + SequencedDiskWriter, +} from "./StreamingFileWriter"; import { FileAssembler } from "./FileAssembler"; import { ProgressReporter, ProgressCallback } from "./ProgressReporter"; import { ReceptionConfig } from "./ReceptionConfig"; @@ -25,9 +28,11 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { private progressReporter: ProgressReporter; // Callbacks - public onFileMetaReceived: ((meta: fileMetadata) => void) | undefined = undefined; + public onFileMetaReceived: ((meta: fileMetadata) => void) | undefined = + undefined; public onStringReceived: ((str: string) => void) | undefined = undefined; - public onFileReceived: ((file: CustomFile) => Promise) | undefined = undefined; + public onFileReceived: ((file: CustomFile) => Promise) | undefined = + undefined; constructor(private webrtcConnection: WebRTC_Recipient) { // Initialize all components @@ -50,7 +55,7 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { this.onStringReceived(str); } }, - log: this.log.bind(this) + log: this.log.bind(this), } ); @@ -94,7 +99,9 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { const fileInfo = this.stateManager.getFileMetadata(fileId); if (!fileInfo) { - this.fireError("File info not found for the requested fileId", { fileId }); + this.fireError("File info not found for the requested fileId", { + fileId, + }); return; } @@ -125,7 +132,9 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { } this.log("log", `Resuming file from offset: ${offset}`, { fileId }); } catch (e) { - this.log("log", "Partial file not found, starting from scratch.", { fileId }); + this.log("log", "Partial file not found, starting from scratch.", { + fileId, + }); offset = 0; } } @@ -138,11 +147,11 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { // 🎯 关键日志2:接收端总结信息 - 使用统一的chunk范围计算逻辑 const chunkRange = ChunkRangeCalculator.getChunkRange( - fileInfo.size, - offset, + fileInfo.size, + offset, ReceptionConfig.FILE_CONFIG.CHUNK_SIZE ); - + postLogToBackend( `[RECV-SUMMARY] File: ${fileInfo.name}, expected: ${expectedChunksCount}, calculated: ${chunkRange.totalChunks}, startChunk: ${chunkRange.startChunk}, endChunk: ${chunkRange.endChunk}, absoluteTotal: ${chunkRange.absoluteTotalChunks}` ); @@ -161,7 +170,9 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { // Send file request const success = this.messageProcessor.sendFileRequest(fileId, offset); if (!success) { - this.stateManager.failFileReception(new Error("Failed to send file request")); + this.stateManager.failFileReception( + new Error("Failed to send file request") + ); return; } @@ -174,7 +185,9 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { public async requestFolder(folderName: string): Promise { const folderProgress = this.stateManager.getFolderProgress(folderName); if (!folderProgress || folderProgress.fileIds.length === 0) { - this.log("warn", "No files found for the requested folder.", { folderName }); + this.log("warn", "No files found for the requested folder.", { + folderName, + }); return; } @@ -185,10 +198,11 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { const fileInfo = this.stateManager.getFileMetadata(fileId); if (fileInfo) { try { - const partialSize = await this.streamingFileWriter.getPartialFileSize( - fileInfo.name, - fileInfo.fullName - ); + const partialSize = + await this.streamingFileWriter.getPartialFileSize( + fileInfo.name, + fileInfo.fullName + ); initialFolderReceivedSize += partialSize; } catch (e) { // File doesn't exist, so its size is 0 @@ -197,7 +211,10 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { } } - this.stateManager.setFolderReceivedSize(folderName, initialFolderReceivedSize); + this.stateManager.setFolderReceivedSize( + folderName, + initialFolderReceivedSize + ); this.log( "log", `Requesting folder, initial received size: ${initialFolderReceivedSize}`, @@ -205,7 +222,7 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { ); this.stateManager.setCurrentFolderName(folderName); - + for (const fileId of folderProgress.fileIds) { try { await this.requestFile(fileId, false); @@ -217,19 +234,23 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { break; } } - + this.stateManager.setCurrentFolderName(null); // Send folder completion message const completedFileIds = folderProgress.fileIds.filter(() => true); // Assume all succeeded - + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { postLogToBackend( `[DEBUG] 📁 All files in folder completed - ${folderName}, files: ${completedFileIds.length}/${folderProgress.fileIds.length}` ); } - this.messageProcessor.sendFolderReceiveComplete(folderName, completedFileIds, true); + this.messageProcessor.sendFolderReceiveComplete( + folderName, + completedFileIds, + true + ); } // ===== MessageProcessorDelegate Implementation ===== @@ -252,8 +273,11 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { */ private setupDataHandler(): void { this.webrtcConnection.onDataReceived = async (data, peerId) => { - const binaryData = await this.messageProcessor.handleReceivedMessage(data, peerId); - + const binaryData = await this.messageProcessor.handleReceivedMessage( + data, + peerId + ); + if (binaryData) { // Handle binary chunk data await this.handleBinaryChunkData(binaryData); @@ -296,7 +320,9 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { /** * Handle embedded chunk packet */ - private async handleEmbeddedChunkPacket(arrayBuffer: ArrayBuffer): Promise { + private async handleEmbeddedChunkPacket( + arrayBuffer: ArrayBuffer + ): Promise { const parsed = this.chunkProcessor.parseEmbeddedChunkPacket(arrayBuffer); if (!parsed) { this.fireError("Failed to parse embedded chunk packet"); @@ -341,10 +367,12 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { } // Check if chunk index is valid - if (!this.chunkProcessor.isChunkIndexValid( - result.relativeChunkIndex, - reception.expectedChunksCount - )) { + if ( + !this.chunkProcessor.isChunkIndexValid( + result.relativeChunkIndex, + reception.expectedChunksCount + ) + ) { if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { postLogToBackend( `[DEBUG-CHUNKS] ❌ Invalid relative chunk index - absolute:${result.absoluteChunkIndex}, relative:${result.relativeChunkIndex}, arraySize:${reception.chunks.length}` @@ -482,6 +510,26 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { `[DEBUG-FINALIZE] ✅ LARGE_FILE finalized successfully - ${reception.meta.name}` ); } + + // 🆕 Send completion confirmation for large files + const stats = this.chunkProcessor.calculateCompletionStats( + reception.chunks, + reception.expectedChunksCount, + reception.meta.size - reception.initialOffset + ); + + this.messageProcessor.sendFileReceiveComplete( + reception.meta.fileId, + stats.currentTotalSize, + stats.sequencedCount, + true + ); + + if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { + postLogToBackend( + `[DEBUG-FINALIZE] 📤 LARGE_FILE completion confirmation sent - ${reception.meta.fileId}, size: ${stats.currentTotalSize}, chunks: ${stats.sequencedCount}` + ); + } } catch (error) { if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { postLogToBackend( @@ -524,7 +572,7 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { offset: number ): Promise { try { - const { fileHandle, writeStream, sequencedWriter } = + const { fileHandle, writeStream, sequencedWriter } = await this.streamingFileWriter.createWriteStream( meta.name, meta.fullName, @@ -585,18 +633,21 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { const reception = this.stateManager.getActiveFileReception(); if (reception?.sequencedWriter && reception?.writeStream) { - this.log( - "log", - "Attempting to gracefully close streams on shutdown." - ); + this.log("log", "Attempting to gracefully close streams on shutdown."); // Close sequenced writer and write stream reception.sequencedWriter.close().catch((err: any) => { - this.log("error", "Error closing sequenced writer during graceful shutdown", { err }); + this.log( + "error", + "Error closing sequenced writer during graceful shutdown", + { err } + ); }); reception.writeStream.close().catch((err: any) => { - this.log("error", "Error closing stream during graceful shutdown", { err }); + this.log("error", "Error closing stream during graceful shutdown", { + err, + }); }); } @@ -662,4 +713,4 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { this.messageProcessor.cleanup(); this.log("log", "FileReceiveOrchestrator cleaned up"); } -} \ No newline at end of file +} From d0ba2eb9c44a9b069a5b00df96b15af8adb37c90 Mon Sep 17 00:00:00 2001 From: david_bai Date: Sun, 14 Sep 2025 19:44:11 +0800 Subject: [PATCH 07/10] chore:Folder resuming transfer is normal --- frontend/lib/transfer/StreamingFileReader.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/frontend/lib/transfer/StreamingFileReader.ts b/frontend/lib/transfer/StreamingFileReader.ts index e015520..2e2d3b8 100644 --- a/frontend/lib/transfer/StreamingFileReader.ts +++ b/frontend/lib/transfer/StreamingFileReader.ts @@ -99,10 +99,12 @@ export class StreamingFileReader { // 🎯 关键日志:边界chunk验证(临时保留用于验证修复效果) if (developmentEnv === "development") { const totalChunks = this.calculateTotalNetworkChunks(); - + // 🔧 修复:使用简化的边界检测逻辑 const isFirst = globalChunkIndex === this.startChunkIndex; - const expectedLastChunk = Math.floor((this.totalFileSize - 1) / this.NETWORK_CHUNK_SIZE); + const expectedLastChunk = Math.floor( + (this.totalFileSize - 1) / this.NETWORK_CHUNK_SIZE + ); const isRealLast = isLast && globalChunkIndex === expectedLastChunk; if (isFirst || isRealLast) { @@ -246,15 +248,16 @@ export class StreamingFileReader { /** * ✂️ Slice 64KB network chunk from 32MB batch + * 🆕 修复:直接基于offset在batch中的位置计算,避免复杂的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; + // 🆕 直接基于offset在batch中的位置计算,避免batch内索引计算错误 + const offsetInBatch = this.totalFileOffset - this.currentBatchStartOffset; + const remainingInBatch = this.currentBatch.byteLength - offsetInBatch; const chunkSize = Math.min(this.NETWORK_CHUNK_SIZE, remainingInBatch); if (chunkSize <= 0) { @@ -262,8 +265,8 @@ export class StreamingFileReader { } const networkChunk = this.currentBatch.slice( - chunkStartInBatch, - chunkStartInBatch + chunkSize + offsetInBatch, + offsetInBatch + chunkSize ); // Delete frequent slice logs, only output when needed From 95331cb8e6f2831e149471a4d1e16ddcc7de9680 Mon Sep 17 00:00:00 2001 From: david_bai Date: Sun, 14 Sep 2025 23:25:16 +0800 Subject: [PATCH 08/10] chore:Remove the redundant safety save button; Use English comments --- frontend/components/ClipboardApp.tsx | 2 -- .../ClipboardApp/FileListDisplay.tsx | 25 ------------------ .../ClipboardApp/RetrieveTabPanel.tsx | 3 --- frontend/constants/messages/de.ts | 8 ++---- frontend/constants/messages/en.ts | 5 ---- frontend/constants/messages/es.ts | 11 +++----- frontend/constants/messages/fr.ts | 8 ++---- frontend/constants/messages/ja.ts | 3 --- frontend/constants/messages/ko.ts | 3 --- frontend/constants/messages/zh.ts | 4 --- frontend/hooks/useWebRTCConnection.ts | 4 +-- frontend/lib/receive/ChunkProcessor.ts | 12 ++++----- .../lib/receive/FileReceiveOrchestrator.ts | 13 ++++------ frontend/lib/transfer/StreamingFileReader.ts | 26 ++++++++----------- frontend/lib/webrtcService.ts | 8 ++---- frontend/types/messages.ts | 3 --- 16 files changed, 32 insertions(+), 106 deletions(-) diff --git a/frontend/components/ClipboardApp.tsx b/frontend/components/ClipboardApp.tsx index 1b08972..5b8ef19 100644 --- a/frontend/components/ClipboardApp.tsx +++ b/frontend/components/ClipboardApp.tsx @@ -55,7 +55,6 @@ const ClipboardApp = () => { requestFolder, setReceiverDirectoryHandle, getReceiverSaveType, - manualSafeSave, } = useWebRTCConnection({ messages, putMessageInMs, @@ -219,7 +218,6 @@ const ClipboardApp = () => { getReceiverSaveType={getReceiverSaveType} retrieveMessage={retrieveMessage} handleLeaveRoom={handleLeaveReceiverRoom} - manualSafeSave={manualSafeSave} /> )} diff --git a/frontend/components/ClipboardApp/FileListDisplay.tsx b/frontend/components/ClipboardApp/FileListDisplay.tsx index 7d2943d..6c9b533 100644 --- a/frontend/components/ClipboardApp/FileListDisplay.tsx +++ b/frontend/components/ClipboardApp/FileListDisplay.tsx @@ -44,7 +44,6 @@ interface FileListDisplayProps { onRequest?: (item: FileMeta) => void; // Request file onDelete?: (item: FileMeta) => void; onLocationPick?: () => Promise; - onSafeSave?: () => void; // New prop for safe save functionality saveType?: { [fileId: string]: boolean }; // File stored on disk or in memory largeFileThreshold?: number; } @@ -66,7 +65,6 @@ const FileListDisplay: React.FC = ({ onRequest, onDelete, onLocationPick, - onSafeSave, saveType, largeFileThreshold = 500 * 1024 * 1024, // 500MB default }) => { @@ -459,29 +457,6 @@ const FileListDisplay: React.FC = ({ {messages.text.FileListDisplay.chooseSavePath_dis} )} - {/* Safe Save Button - only show when location is picked and files are saved to disk */} - {onSafeSave && - pickedLocation && - (isAnyFileTransferring || - (saveType && - Object.values(saveType).some( - (isSavedToDisk) => isSavedToDisk - ))) && ( - - - - )} )} diff --git a/frontend/components/ClipboardApp/RetrieveTabPanel.tsx b/frontend/components/ClipboardApp/RetrieveTabPanel.tsx index 989223c..0428b6c 100644 --- a/frontend/components/ClipboardApp/RetrieveTabPanel.tsx +++ b/frontend/components/ClipboardApp/RetrieveTabPanel.tsx @@ -30,7 +30,6 @@ interface RetrieveTabPanelProps { directoryHandle: FileSystemDirectoryHandle ) => Promise; getReceiverSaveType: () => { [fileId: string]: boolean } | undefined; - manualSafeSave: () => void; // Add manual safe save function retrieveMessage: string; handleLeaveRoom: () => void; } @@ -47,7 +46,6 @@ export function RetrieveTabPanel({ requestFolder, setReceiverDirectoryHandle, getReceiverSaveType, - manualSafeSave, retrieveMessage, handleLeaveRoom, }: RetrieveTabPanelProps) { @@ -168,7 +166,6 @@ export function RetrieveTabPanel({ onDownload={handleDownloadFile} onRequest={handleFileRequestFromPanel} // Use the panel's own handler onLocationPick={onLocationPick} // Use the panel's own handler - onSafeSave={manualSafeSave} // Add safe save handler saveType={getReceiverSaveType()} /> {retrieveMessage && ( diff --git a/frontend/constants/messages/de.ts b/frontend/constants/messages/de.ts index e7c70cd..300189f 100644 --- a/frontend/constants/messages/de.ts +++ b/frontend/constants/messages/de.ts @@ -256,11 +256,6 @@ export const de: Messages = { chooseSavePath_tips: "Speichern Sie große Dateien oder Ordner direkt in einem ausgewählten Verzeichnis. 👉", chooseSavePath_dis: "Speicherort auswählen", - safeSave_dis: "Sicheres Speichern", - safeSave_tooltip: - "Keine Angst vor Verbindungsunterbrechung, klicken Sie hier, um Dateien sicher zu speichern für die nächste Fortsetzung", - safeSaveSuccessMsg: - "Dateien wurden sicher auf der Festplatte gespeichert, sicher die Seite zu schließen, unterstützt Wiederaufnahme der Übertragung!", }, RetrieveMethod: { P: "Glückwunsch 🎉 Freigegebene Inhalte warten darauf, abgerufen zu werden:", @@ -316,7 +311,8 @@ export const de: Messages = { noFilesForFolderMsg: "Keine Dateien im Ordner '{folderName}' gefunden.", zipError: "Fehler beim Erstellen der ZIP-Datei.", fileNotFoundMsg: "Datei '{fileName}' zum Herunterladen nicht gefunden.", - confirmLeaveWhileTransferring: "Dateien werden derzeit übertragen. Das Verlassen wird die Übertragung unterbrechen. Sind Sie sicher?", + confirmLeaveWhileTransferring: + "Dateien werden derzeit übertragen. Das Verlassen wird die Übertragung unterbrechen. Sind Sie sicher?", leaveWhileTransferringSuccess: "Raum verlassen, Übertragung unterbrochen", html: { senderTab: "Senden", diff --git a/frontend/constants/messages/en.ts b/frontend/constants/messages/en.ts index 5d7ad00..dc4fc83 100644 --- a/frontend/constants/messages/en.ts +++ b/frontend/constants/messages/en.ts @@ -253,11 +253,6 @@ export const en: Messages = { chooseSavePath_tips: "Save large files or folders directly to a selected directory. 👉", chooseSavePath_dis: "Choose save location", - safeSave_dis: "Safe Save", - safeSave_tooltip: - "Don't worry about connection interruption, click here to safely save files for next resume", - safeSaveSuccessMsg: - "Files have been safely saved to disk, safe to close page, supports resume transfer!", }, RetrieveMethod: { P: "Congrats 🎉 Share content is waiting to be retrieved:", diff --git a/frontend/constants/messages/es.ts b/frontend/constants/messages/es.ts index 8e605a2..e1e014d 100644 --- a/frontend/constants/messages/es.ts +++ b/frontend/constants/messages/es.ts @@ -254,11 +254,6 @@ export const es: Messages = { chooseSavePath_tips: "Guarda archivos grandes o carpetas directamente en un directorio seleccionado. 👉", chooseSavePath_dis: "Elegir ubicación de guardado", - safeSave_dis: "Guardar Seguro", - safeSave_tooltip: - "No te preocupes por la interrupción de la conexión, haz clic aquí para guardar archivos de forma segura para la próxima reanudación", - safeSaveSuccessMsg: - "Los archivos se han guardado de forma segura en el disco, es seguro cerrar la página, ¡admite transferencia de reanudación!", }, RetrieveMethod: { P: "¡Felicitaciones 🎉 El contenido compartido está esperando ser recuperado:", @@ -310,8 +305,10 @@ export const es: Messages = { "No se encontraron archivos en la carpeta '{folderName}'.", zipError: "Error al crear el archivo ZIP.", fileNotFoundMsg: "Archivo '{fileName}' no encontrado para descargar.", - confirmLeaveWhileTransferring: "Los archivos se están transfiriendo actualmente. Salir interrumpirá la transferencia. ¿Estás seguro?", - leaveWhileTransferringSuccess: "Saliste de la sala, transferencia interrumpida", + confirmLeaveWhileTransferring: + "Los archivos se están transfiriendo actualmente. Salir interrumpirá la transferencia. ¿Estás seguro?", + leaveWhileTransferringSuccess: + "Saliste de la sala, transferencia interrumpida", html: { senderTab: "Enviar", retrieveTab: "Recuperar", diff --git a/frontend/constants/messages/fr.ts b/frontend/constants/messages/fr.ts index 63de6c4..b34fc01 100644 --- a/frontend/constants/messages/fr.ts +++ b/frontend/constants/messages/fr.ts @@ -257,11 +257,6 @@ export const fr: Messages = { chooseSavePath_tips: "Enregistrez des fichiers volumineux ou des dossiers directement dans un répertoire sélectionné. 👉", chooseSavePath_dis: "Choisir l'emplacement de sauvegarde", - safeSave_dis: "Sauvegarde Sécurisée", - safeSave_tooltip: - "N'ayez pas peur de l'interruption de connexion, cliquez ici pour sauvegarder les fichiers en toute sécurité pour la prochaine reprise", - safeSaveSuccessMsg: - "Les fichiers ont été sauvegardés en toute sécurité sur le disque, sûr de fermer la page, prend en charge la reprise du transfert !", }, RetrieveMethod: { P: "Félicitations 🎉 Le contenu partagé attend d'être récupéré :", @@ -318,7 +313,8 @@ export const fr: Messages = { zipError: "Erreur lors de la création du fichier ZIP.", fileNotFoundMsg: "Fichier '{fileName}' introuvable pour le téléchargement.", - confirmLeaveWhileTransferring: "Des fichiers sont actuellement en cours de transfert. Quitter interrompra le transfert. Êtes-vous sûr?", + confirmLeaveWhileTransferring: + "Des fichiers sont actuellement en cours de transfert. Quitter interrompra le transfert. Êtes-vous sûr?", leaveWhileTransferringSuccess: "Salle quittée, transfert interrompu", html: { senderTab: "Envoyer", diff --git a/frontend/constants/messages/ja.ts b/frontend/constants/messages/ja.ts index fa27900..4e9f172 100644 --- a/frontend/constants/messages/ja.ts +++ b/frontend/constants/messages/ja.ts @@ -249,9 +249,6 @@ export const ja: Messages = { chooseSavePath_tips: "大きなファイルやフォルダを選択したディレクトリに直接保存します。👉", chooseSavePath_dis: "保存場所を選択", - safeSave_dis: "安全保存", - safeSave_tooltip: "接続の中断を恐れる必要はありません。ここをクリックして、次回の再開のためにファイルを安全に保存してください", - safeSaveSuccessMsg: "ファイルが安全にディスクに保存されました。ページを安全に閉じることができ、転送の再開をサポートします!", }, RetrieveMethod: { P: "おめでとう 🎉 共有コンテンツが取得待ちです:", diff --git a/frontend/constants/messages/ko.ts b/frontend/constants/messages/ko.ts index 68982f0..9ba0b8c 100644 --- a/frontend/constants/messages/ko.ts +++ b/frontend/constants/messages/ko.ts @@ -247,9 +247,6 @@ export const ko: Messages = { chooseSavePath_tips: "큰 파일이나 폴더를 선택한 디렉터리에 직접 저장합니다. 👉", chooseSavePath_dis: "저장 위치 선택", - safeSave_dis: "안전 저장", - safeSave_tooltip: "연결 중단을 두려워하지 마세요. 다음 재개를 위해 파일을 안전하게 저장하려면 여기를 클릭하세요", - safeSaveSuccessMsg: "파일이 디스크에 안전하게 저장되었습니다. 페이지를 안전하게 닫을 수 있으며 전송 재개를 지원합니다!", }, RetrieveMethod: { P: "축하합니다 🎉 공유된 콘텐츠가 검색을 기다리고 있습니다:", diff --git a/frontend/constants/messages/zh.ts b/frontend/constants/messages/zh.ts index d0240e1..b3c7b0c 100644 --- a/frontend/constants/messages/zh.ts +++ b/frontend/constants/messages/zh.ts @@ -235,10 +235,6 @@ export const zh: Messages = { "我们建议选择一个保存目录来直接将文件保存到磁盘。这样可以更方便地传输大文件和同步文件夹。", chooseSavePath_tips: "大文件或文件夹可直接保存到指定目录 👉", chooseSavePath_dis: "选择保存位置", - safeSave_dis: "安全保存", - safeSave_tooltip: "连接中断不要怕,点击这里安全保存文件,方便下次续传", - safeSaveSuccessMsg: - "文件已安全保存到磁盘,可以安全关闭页面,支持断点续传!", }, RetrieveMethod: { P: "恭喜 🎉 共享内容等待接收:", diff --git a/frontend/hooks/useWebRTCConnection.ts b/frontend/hooks/useWebRTCConnection.ts index c8636c2..216080d 100644 --- a/frontend/hooks/useWebRTCConnection.ts +++ b/frontend/hooks/useWebRTCConnection.ts @@ -18,8 +18,7 @@ interface UseWebRTCConnectionProps { } export function useWebRTCConnection({ - messages, - putMessageInMs, + // Retaining interface compatibility but these are no longer used }: UseWebRTCConnectionProps) { // Get state from store const { @@ -64,7 +63,6 @@ export function useWebRTCConnection({ setReceiverDirectoryHandle: webrtcService.setReceiverDirectoryHandle.bind(webrtcService), getReceiverSaveType: webrtcService.getReceiverSaveType.bind(webrtcService), - manualSafeSave: webrtcService.manualSafeSave.bind(webrtcService), // Reset connection methods resetSenderConnection: () => webrtcService.leaveRoom(true), diff --git a/frontend/lib/receive/ChunkProcessor.ts b/frontend/lib/receive/ChunkProcessor.ts index 853fabc..95f0ae5 100644 --- a/frontend/lib/receive/ChunkProcessor.ts +++ b/frontend/lib/receive/ChunkProcessor.ts @@ -2,8 +2,6 @@ import { EmbeddedChunkMeta } from "@/types/webrtc"; import { ReceptionConfig } from "./ReceptionConfig"; import { postLogToBackend } from "@/app/config/api"; -const developmentEnv = process.env.NODE_ENV; - /** * 🚀 Chunk processing result interface */ @@ -148,7 +146,7 @@ export class ChunkProcessor { const startChunkIndex = ReceptionConfig.getChunkIndexFromOffset(initialOffset); // Resume start index const relativeChunkIndex = absoluteChunkIndex - startChunkIndex; // Relative index in chunks array - // 🎯 简化调试:只在边界chunk时记录索引映射 + // 🎯 Simplify debugging: Only record index mapping when boundary chunk if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING && (absoluteChunkIndex <= 2 || relativeChunkIndex <= 2)) { postLogToBackend( `[INDEX-MAP] abs:${absoluteChunkIndex}, start:${startChunkIndex}, rel:${relativeChunkIndex}` @@ -196,7 +194,7 @@ export class ChunkProcessor { const startChunkIndex = ReceptionConfig.getChunkIndexFromOffset(initialOffset); const calculatedExpected = chunkMeta.totalChunks - startChunkIndex; - // 🎯 简化日志:只在数量不匹配时记录关键信息 + // 🎯 Simplify logging: Only record critical information when the number does not match if (chunkMeta.totalChunks !== expectedChunksCount && calculatedExpected !== expectedChunksCount) { if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { postLogToBackend( @@ -233,12 +231,12 @@ export class ChunkProcessor { return; } - // 🎯 简化日志:只记录边界chunk和异常情况 + // 🎯 Simplify logging: Only record boundary chunk and abnormal cases const { chunkMeta, absoluteChunkIndex, relativeChunkIndex } = result; const isFirstFew = absoluteChunkIndex <= 3; const isLastFew = relativeChunkIndex >= expectedChunksCount - 3; - // 🔧 修复:SequencedWriter期望的是绝对索引,不是相对索引 + // 🔧 Fix: SequencedWriter expects absolute index, not relative index const hasIndexMismatch = writerExpectedIndex !== undefined && absoluteChunkIndex !== writerExpectedIndex; if (isFirstFew || isLastFew || hasIndexMismatch) { @@ -311,7 +309,7 @@ export class ChunkProcessor { const { sequencedCount, expectedChunksCount, currentTotalSize, expectedSize, isDataComplete } = stats; - // 🎯 关键日志3:只在完成时打印最终检查结果 + // 🎯 Critical log 3: Only print final check results when complete if (isDataComplete) { const startChunkIndex = ReceptionConfig.getChunkIndexFromOffset(initialOffset); const missingChunks = []; diff --git a/frontend/lib/receive/FileReceiveOrchestrator.ts b/frontend/lib/receive/FileReceiveOrchestrator.ts index ffa4a34..92ba237 100644 --- a/frontend/lib/receive/FileReceiveOrchestrator.ts +++ b/frontend/lib/receive/FileReceiveOrchestrator.ts @@ -2,10 +2,9 @@ import WebRTC_Recipient from "../webrtc_Recipient"; import { CustomFile, fileMetadata } from "@/types/webrtc"; import { ReceptionStateManager } from "./ReceptionStateManager"; import { MessageProcessor, MessageProcessorDelegate } from "./MessageProcessor"; -import { ChunkProcessor, ChunkProcessingResult } from "./ChunkProcessor"; +import { ChunkProcessor } from "./ChunkProcessor"; import { - StreamingFileWriter, - SequencedDiskWriter, + StreamingFileWriter } from "./StreamingFileWriter"; import { FileAssembler } from "./FileAssembler"; import { ProgressReporter, ProgressCallback } from "./ProgressReporter"; @@ -13,8 +12,6 @@ import { ReceptionConfig } from "./ReceptionConfig"; import { ChunkRangeCalculator } from "@/lib/utils/ChunkRangeCalculator"; import { postLogToBackend } from "@/app/config/api"; -const developmentEnv = process.env.NODE_ENV; - /** * 🚀 File receive orchestrator * Main coordinator that integrates all reception modules @@ -145,7 +142,7 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { ); if (ReceptionConfig.DEBUG_CONFIG.ENABLE_CHUNK_LOGGING) { - // 🎯 关键日志2:接收端总结信息 - 使用统一的chunk范围计算逻辑 + // 🎯 Critical log 2: Summary information for receiver - using unified chunk range calculation logic const chunkRange = ChunkRangeCalculator.getChunkRange( fileInfo.size, offset, @@ -395,14 +392,14 @@ export class FileReceiveOrchestrator implements MessageProcessorDelegate { // Handle disk writing if needed if (reception.sequencedWriter) { - // 🔧 修复:SequencedWriter使用绝对索引,确保传递正确的索引 + // 🔧 Fix: SequencedWriter uses absolute index, ensuring correct index is passed this.chunkProcessor.logChunkDetails( result, reception.expectedChunksCount, reception.sequencedWriter.expectedIndex ); - // ✅ 正确使用绝对索引进行磁盘写入 + // ✅ Correctly use absolute index for disk writing await reception.sequencedWriter.writeChunk( result.absoluteChunkIndex, result.chunkData diff --git a/frontend/lib/transfer/StreamingFileReader.ts b/frontend/lib/transfer/StreamingFileReader.ts index 2e2d3b8..6f1329c 100644 --- a/frontend/lib/transfer/StreamingFileReader.ts +++ b/frontend/lib/transfer/StreamingFileReader.ts @@ -40,7 +40,7 @@ export class StreamingFileReader { // Global state private totalFileOffset = 0; // Current position in the entire file - private startChunkIndex = 0; // 🔧 记录传输起始的chunk索引 + private startChunkIndex = 0; // 🔧 Record the chunk index at the start of transmission private isFinished = false; private isReading = false; // Prevent concurrent reading @@ -48,15 +48,14 @@ export class StreamingFileReader { this.file = file; this.totalFileSize = file.size; this.totalFileOffset = startOffset; - // 🔧 修复:续传时currentBatchStartOffset应该从startOffset开始 + // 🔧 Fix: When resuming, currentBatchStartOffset should start from startOffset this.currentBatchStartOffset = startOffset; this.fileReader = new FileReader(); - // 🔧 记录传输的起始chunk索引,用于边界检测 + // 🔧 Record the starting chunk index of the transfer, used for boundary detection this.startChunkIndex = Math.floor(startOffset / this.NETWORK_CHUNK_SIZE); if (developmentEnv === "development") { - // 🎯 关键日志1:发送端总结信息 - 使用统一的chunk范围计算逻辑 const chunkRange = ChunkRangeCalculator.getChunkRange( this.totalFileSize, startOffset, @@ -96,11 +95,9 @@ export class StreamingFileReader { // 4. Update state this.updateChunkState(networkChunk); - // 🎯 关键日志:边界chunk验证(临时保留用于验证修复效果) if (developmentEnv === "development") { const totalChunks = this.calculateTotalNetworkChunks(); - // 🔧 修复:使用简化的边界检测逻辑 const isFirst = globalChunkIndex === this.startChunkIndex; const expectedLastChunk = Math.floor( (this.totalFileSize - 1) / this.NETWORK_CHUNK_SIZE @@ -188,9 +185,9 @@ export class StreamingFileReader { const batchStartOffset = this.totalFileOffset; this.currentBatchStartOffset = batchStartOffset; - // 🔧 修复:简化batch内索引计算逻辑 - // 由于calculateGlobalChunkIndex现在直接基于totalFileOffset计算, - // batch内索引只需要基于当前batch的起始位置计算即可 + // 🔧 Fix: Simplify index calculation logic within batch + // Since calculateGlobalChunkIndex now directly calculates based on totalFileOffset + // Indexing within a batch only needs to be calculated based on the starting position of the current batch const chunkOffsetInBatch = batchStartOffset - Math.floor(batchStartOffset / this.BATCH_SIZE) * this.BATCH_SIZE; @@ -248,14 +245,14 @@ export class StreamingFileReader { /** * ✂️ Slice 64KB network chunk from 32MB batch - * 🆕 修复:直接基于offset在batch中的位置计算,避免复杂的batch内索引计算 + * 🆕 Fix: Calculate directly based on the position of offset in the batch, avoiding complex batch internal index calculations */ private sliceNetworkChunkFromBatch(): ArrayBuffer { if (!this.currentBatch) { throw new Error("No current batch available for slicing"); } - // 🆕 直接基于offset在batch中的位置计算,避免batch内索引计算错误 + // 🆕 Calculated directly based on the position of offset in the batch to avoid index calculation errors within the batch const offsetInBatch = this.totalFileOffset - this.currentBatchStartOffset; const remainingInBatch = this.currentBatch.byteLength - offsetInBatch; const chunkSize = Math.min(this.NETWORK_CHUNK_SIZE, remainingInBatch); @@ -278,8 +275,7 @@ export class StreamingFileReader { * 🔧 Simplified logic: directly calculate based on file offset to avoid batch boundary errors */ private calculateGlobalChunkIndex(): number { - // 🎯 核心修复:直接基于当前文件偏移量计算chunk索引,避免复杂的batch计算 - // 这确保了与接收端ReceptionConfig.getChunkIndexFromOffset()完全一致的计算逻辑 + // Calculate chunk index directly based on current file offset, avoiding complex batch calculations, consistent with receiver return Math.floor(this.totalFileOffset / this.NETWORK_CHUNK_SIZE); } @@ -357,9 +353,9 @@ export class StreamingFileReader { this.isFinished = false; this.isReading = false; this.currentBatch = null; - // 🔧 修复:reset时也要正确设置currentBatchStartOffset + // 🔧 Fix: Reset also needs to correctly set currentBatchStartOffset this.currentBatchStartOffset = startOffset; - this.currentChunkIndexInBatch = 0; // 重置为0,loadNextBatch会重新计算 + this.currentChunkIndexInBatch = 0; // Reset to 0, loadNextBatch will recalculate if (developmentEnv === "development") { postLogToBackend( diff --git a/frontend/lib/webrtcService.ts b/frontend/lib/webrtcService.ts index 40e7564..5257327 100644 --- a/frontend/lib/webrtcService.ts +++ b/frontend/lib/webrtcService.ts @@ -60,7 +60,7 @@ class WebRTCService { } }; - this.sender.onDataChannelOpen = (peerId) => { + this.sender.onDataChannelOpen = (_peerId) => { useFileTransferStore.getState().setIsSenderInRoom(true); // Automatically broadcast current content this.broadcastDataToAllPeers(); @@ -215,10 +215,6 @@ class WebRTCService { return this.fileReceiver.saveType; } - public manualSafeSave(): void { - this.fileReceiver.gracefulShutdown(); - } - private handleConnectionDisconnect(peerId: string, isSender: boolean, reason: string): void { console.log(`[WebRTC Service] Connection disconnect: ${reason}, peer: ${peerId}, sender: ${isSender}`); @@ -256,7 +252,7 @@ class WebRTCService { } // update connection state - private updateConnectionState(peerId: string, isSender: boolean): void { + private updateConnectionState(_peerId: string, isSender: boolean): void { const store = useFileTransferStore.getState(); if (isSender) { diff --git a/frontend/types/messages.ts b/frontend/types/messages.ts index bb6926e..02c59f2 100644 --- a/frontend/types/messages.ts +++ b/frontend/types/messages.ts @@ -193,9 +193,6 @@ export type FileListDisplay = { PopupDialog_description: string; chooseSavePath_tips: string; chooseSavePath_dis: string; - safeSave_dis: string; - safeSave_tooltip: string; - safeSaveSuccessMsg: string; }; export type RetrieveMethod = { From 55f118be9a61eb4c662f40d499fc9fdc24110644 Mon Sep 17 00:00:00 2001 From: david_bai Date: Sun, 14 Sep 2025 23:30:47 +0800 Subject: [PATCH 09/10] chore:Temporarily comment out some debug logs --- .../lib/transfer/FileTransferOrchestrator.ts | 6 +-- frontend/lib/transfer/NetworkTransmitter.ts | 44 +++++++++---------- frontend/lib/transfer/StreamingFileReader.ts | 26 +++++------ 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/frontend/lib/transfer/FileTransferOrchestrator.ts b/frontend/lib/transfer/FileTransferOrchestrator.ts index 832cfab..e69b01e 100644 --- a/frontend/lib/transfer/FileTransferOrchestrator.ts +++ b/frontend/lib/transfer/FileTransferOrchestrator.ts @@ -205,7 +205,7 @@ export class FileTransferOrchestrator implements MessageHandlerDelegate { const peerState = this.stateManager.getPeerState(peerId); const transferStartTime = performance.now(); - // 🔧 修复:记录传输开始时的初始offset,用于后续统计计算 + // 🔧 Fix: Record initial offset at the start of transmission, used for subsequent statistics calculation const initialReadOffset = peerState.readOffset || 0; // 1. Initialize streaming file reader @@ -305,8 +305,8 @@ export class FileTransferOrchestrator implements MessageHandlerDelegate { const totalTime = performance.now() - transferStartTime; const avgSpeedMBps = totalBytesSent / 1024 / 1024 / (totalTime / 1000); - // 🔧 修复:使用正确的初始offset而不是当前readOffset来计算日志统计 - const initialOffset = initialReadOffset || 0; // 传输开始时的offset + // 🔧 Fix: Use correct initial offset instead of current readOffset for log statistics + const initialOffset = initialReadOffset || 0; // Initial offset at the start of transmission const expectedTotalChunks = Math.ceil(file.size / 65536); const startChunkIndex = Math.floor(initialOffset / 65536); const expectedChunksSent = expectedTotalChunks - startChunkIndex; diff --git a/frontend/lib/transfer/NetworkTransmitter.ts b/frontend/lib/transfer/NetworkTransmitter.ts index fa897f2..d112366 100644 --- a/frontend/lib/transfer/NetworkTransmitter.ts +++ b/frontend/lib/transfer/NetworkTransmitter.ts @@ -33,18 +33,18 @@ export class NetworkTransmitter { // Key node logs (development environment only) - if ( - developmentEnv === "development" && - (metadata.chunkIndex % 100 === 0 || metadata.isLastChunk) - ) { - postLogToBackend( - `[DEBUG] ✓ CHUNK #${metadata.chunkIndex}/${ - metadata.totalChunks - } sent, size: ${(chunkData.byteLength / 1024).toFixed( - 1 - )}KB, isLast: ${metadata.isLastChunk}` - ); - } + // if ( + // developmentEnv === "development" && + // (metadata.chunkIndex % 100 === 0 || metadata.isLastChunk) + // ) { + // postLogToBackend( + // `[DEBUG] ✓ CHUNK #${metadata.chunkIndex}/${ + // metadata.totalChunks + // } sent, size: ${(chunkData.byteLength / 1024).toFixed( + // 1 + // )}KB, isLast: ${metadata.isLastChunk}` + // ); + // } return true; } catch (error) { @@ -148,16 +148,16 @@ export class NetworkTransmitter { }); // Only output backpressure logs in development environment - if (developmentEnv === "development") { - const waitTime = performance.now() - startTime; - postLogToBackend( - `[DEBUG] 🚀 BACKPRESSURE - wait: ${waitTime.toFixed( - 1 - )}ms, buffered: ${(initialBuffered / 1024).toFixed(0)}KB -> ${( - dataChannel.bufferedAmount / 1024 - ).toFixed(0)}KB` - ); - } + // if (developmentEnv === "development") { + // const waitTime = performance.now() - startTime; + // postLogToBackend( + // `[DEBUG] 🚀 BACKPRESSURE - wait: ${waitTime.toFixed( + // 1 + // )}ms, buffered: ${(initialBuffered / 1024).toFixed(0)}KB -> ${( + // dataChannel.bufferedAmount / 1024 + // ).toFixed(0)}KB` + // ); + // } } } diff --git a/frontend/lib/transfer/StreamingFileReader.ts b/frontend/lib/transfer/StreamingFileReader.ts index 6f1329c..ac0cbe8 100644 --- a/frontend/lib/transfer/StreamingFileReader.ts +++ b/frontend/lib/transfer/StreamingFileReader.ts @@ -95,21 +95,21 @@ export class StreamingFileReader { // 4. Update state this.updateChunkState(networkChunk); - if (developmentEnv === "development") { - const totalChunks = this.calculateTotalNetworkChunks(); + // if (developmentEnv === "development") { + // const totalChunks = this.calculateTotalNetworkChunks(); - const isFirst = globalChunkIndex === this.startChunkIndex; - const expectedLastChunk = Math.floor( - (this.totalFileSize - 1) / this.NETWORK_CHUNK_SIZE - ); - const isRealLast = isLast && globalChunkIndex === expectedLastChunk; + // const isFirst = globalChunkIndex === this.startChunkIndex; + // const expectedLastChunk = Math.floor( + // (this.totalFileSize - 1) / this.NETWORK_CHUNK_SIZE + // ); + // const isRealLast = isLast && globalChunkIndex === expectedLastChunk; - if (isFirst || isRealLast) { - postLogToBackend( - `[BOUNDARY] Chunk #${globalChunkIndex}/${totalChunks}, isFirst: ${isFirst}, isLast: ${isRealLast}, startIdx: ${this.startChunkIndex}, expectedLastIdx: ${expectedLastChunk}, size: ${networkChunk.byteLength}` - ); - } - } + // if (isFirst || isRealLast) { + // postLogToBackend( + // `[BOUNDARY] Chunk #${globalChunkIndex}/${totalChunks}, isFirst: ${isFirst}, isLast: ${isRealLast}, startIdx: ${this.startChunkIndex}, expectedLastIdx: ${expectedLastChunk}, size: ${networkChunk.byteLength}` + // ); + // } + // } return { chunk: networkChunk, From 3f075c4a97c66f9b968b88c6bd38b489b93b6466 Mon Sep 17 00:00:00 2001 From: david_bai Date: Sun, 14 Sep 2025 23:47:42 +0800 Subject: [PATCH 10/10] fix:Fix a Type error --- frontend/lib/webrtc_base.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/frontend/lib/webrtc_base.ts b/frontend/lib/webrtc_base.ts index 64be02e..9b9bce6 100644 --- a/frontend/lib/webrtc_base.ts +++ b/frontend/lib/webrtc_base.ts @@ -388,9 +388,11 @@ export default class BaseWebRTC { dataChannel.onerror = (error) => { // Check if this is a user-initiated disconnect (not a real error) - const isUserDisconnect = - error.error?.message?.includes("User-Initiated Abort") || - error.error?.message?.includes("Close called"); + // The error parameter is an Event object, not an Error object + const errorTarget = error.target as RTCDataChannel; + const isUserDisconnect = + errorTarget?.readyState === "closed" || + error.type === "error"; if (isUserDisconnect) { this.log("log", `Data channel closed by user for peer ${peerId}`, { @@ -486,13 +488,13 @@ export default class BaseWebRTC { if (dataChannel?.readyState === "open") { try { // Firefox compatibility debugging: Log sending details - const dataType = + const _dataType = typeof data === "string" ? "string" : data instanceof ArrayBuffer ? "ArrayBuffer" : typeof data; - const dataSize = + const _dataSize = typeof data === "string" ? data.length : data instanceof ArrayBuffer @@ -628,7 +630,7 @@ export default class BaseWebRTC { ); } // Abstract method declaration - protected createDataChannel(peerId: string) { + protected createDataChannel(_peerId: string) { throw new Error("createDataChannel must be implemented by subclass"); } }