chore:Exit the room even if it is in transit
This commit is contained in:
@@ -282,7 +282,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
|
||||
} else {
|
||||
if (developmentEnv === "true") {
|
||||
postLogToBackend(
|
||||
`[Firefox Debug] Skipping download logic - isSaveToDisk: ${isSaveToDisk}, onDownload: ${!!onDownload}`
|
||||
`Skipping download logic - isSaveToDisk: ${isSaveToDisk}, onDownload: ${!!onDownload}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<boolean> => {
|
||||
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}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant={isAnyFileTransferring ? "destructive" : "outline"}
|
||||
onClick={handleLeaveRoom}
|
||||
disabled={!isReceiverInRoom || isAnyFileTransferring}
|
||||
disabled={!isReceiverInRoom}
|
||||
className="w-full sm:w-auto px-4 order-2"
|
||||
>
|
||||
{messages.text.ClipboardApp.roomStatus.leaveRoomBtn}
|
||||
{isAnyFileTransferring
|
||||
? messages.text.ClipboardApp.roomStatus.leaveRoomBtn + " ⚠️"
|
||||
: messages.text.ClipboardApp.roomStatus.leaveRoomBtn}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
</AnimatedButton>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant={isAnyFileTransferring ? "destructive" : "outline"}
|
||||
onClick={handleLeaveSenderRoom}
|
||||
disabled={!isSenderInRoom || isAnyFileTransferring}
|
||||
disabled={!isSenderInRoom}
|
||||
className="w-full sm:w-auto px-4 order-2"
|
||||
>
|
||||
{messages.text.ClipboardApp.roomStatus.leaveRoomBtn}
|
||||
{isAnyFileTransferring
|
||||
? messages.text.ClipboardApp.roomStatus.leaveRoomBtn + " ⚠️"
|
||||
: messages.text.ClipboardApp.roomStatus.leaveRoomBtn}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -302,6 +302,8 @@ export const ja: Messages = {
|
||||
noFilesForFolderMsg: "フォルダ '{folderName}' にファイルが見つかりません。",
|
||||
zipError: "ZIP の作成中にエラーが発生しました。",
|
||||
fileNotFoundMsg: "ダウンロードするファイル '{fileName}' が見つかりません。",
|
||||
confirmLeaveWhileTransferring: "現在ファイルが転送中です。退出すると転送が中断されます。よろしいですか?",
|
||||
leaveWhileTransferringSuccess: "ルームを退出しました。転送が中断されました",
|
||||
html: {
|
||||
senderTab: "送信",
|
||||
retrieveTab: "取得",
|
||||
|
||||
@@ -300,6 +300,8 @@ export const ko: Messages = {
|
||||
noFilesForFolderMsg: "폴더 '{folderName}'에서 파일을 찾을 수 없습니다.",
|
||||
zipError: "ZIP 파일 생성 중 오류가 발생했습니다.",
|
||||
fileNotFoundMsg: "다운로드할 파일 '{fileName}'을(를) 찾을 수 없습니다.",
|
||||
confirmLeaveWhileTransferring: "현재 파일이 전송 중입니다. 나가면 전송이 중단됩니다. 확실합니까?",
|
||||
leaveWhileTransferringSuccess: "방을 나갔습니다. 전송이 중단되었습니다",
|
||||
html: {
|
||||
senderTab: "보내기",
|
||||
retrieveTab: "검색",
|
||||
|
||||
@@ -287,6 +287,8 @@ export const zh: Messages = {
|
||||
noFilesForFolderMsg: "在文件夹 '{folderName}' 中未找到文件。",
|
||||
zipError: "创建 ZIP 文件时出错。",
|
||||
fileNotFoundMsg: "未找到要下载的文件 '{fileName}'。",
|
||||
confirmLeaveWhileTransferring: "当前有文件正在传输,退出将中断传输。确定要退出吗?",
|
||||
leaveWhileTransferringSuccess: "已退出房间,传输已中断",
|
||||
html: {
|
||||
senderTab: "发送",
|
||||
retrieveTab: "接收",
|
||||
|
||||
@@ -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(", ")}]`
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
+143
-51
@@ -80,28 +80,46 @@ class SequencedDiskWriter {
|
||||
* Write current chunk and attempt to sequentially write subsequent chunks
|
||||
*/
|
||||
private async flushSequentialChunks(firstChunk: ArrayBuffer): Promise<void> {
|
||||
// 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<void> {
|
||||
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<void> {
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, PeerState>();
|
||||
private pendingFiles = new Map<string, CustomFile>();
|
||||
private pendingFolderMeta: Record<string, FolderMeta> = {};
|
||||
private networkPerformance = new Map<string, NetworkPerformanceMetrics>();
|
||||
|
||||
// ===== 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+134
-24
@@ -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<void> {
|
||||
// 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<void> {
|
||||
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<void> {
|
||||
console.log("[WebRTC Service] Starting cleanup...");
|
||||
try {
|
||||
|
||||
@@ -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,
|
||||
|
||||
+50
-12
@@ -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<string>;
|
||||
|
||||
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<void> {
|
||||
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",
|
||||
|
||||
@@ -238,14 +238,7 @@ export const useFileTransferStore = create<FileTransferState>()((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: [],
|
||||
|
||||
@@ -275,6 +275,8 @@ export type ClipboardApp = {
|
||||
noFilesForFolderMsg?: string;
|
||||
zipError?: string;
|
||||
fileNotFoundMsg?: string;
|
||||
confirmLeaveWhileTransferring: string;
|
||||
leaveWhileTransferringSuccess: string;
|
||||
};
|
||||
|
||||
export type Home = {
|
||||
|
||||
Reference in New Issue
Block a user