refactor:Optimize chaotic state management

This commit is contained in:
david_bai
2025-08-17 15:44:59 +08:00
parent cbbfae2733
commit caa861f1bb
6 changed files with 450 additions and 688 deletions
+4 -11
View File
@@ -49,10 +49,8 @@ const ClipboardApp = () => {
handleDownloadFile,
} = useFileTransferHandler({ messages, putMessageInMs });
// Initialize WebRTC Connection Hook
// 简化的 WebRTC 连接初始化
const {
sender,
receiver,
sharePeerCount,
retrievePeerCount,
broadcastDataToAllPeers,
@@ -87,7 +85,7 @@ const ClipboardApp = () => {
}
}, [resetReceiverConnection, setRetrieveRoomIdInput]);
// Initialize Room Manager Hook
// 大大简化的房间管理 - 不再需要传递任何 WebRTC 依赖
const {
processRoomIdInput,
joinRoom,
@@ -97,11 +95,6 @@ const ClipboardApp = () => {
} = useRoomManager({
messages,
putMessageInMs,
sender,
receiver,
broadcastDataToPeers: broadcastDataToAllPeers,
resetSenderConnection,
resetReceiverConnection,
});
const handleFileDrop = useCallback(
@@ -231,7 +224,7 @@ const ClipboardApp = () => {
processRoomIdInput={processRoomIdInput}
joinRoom={joinRoom}
generateShareLinkAndBroadcast={generateShareLinkAndBroadcast}
sender={sender}
shareMessage={shareMessage}
currentValidatedShareRoomId={shareRoomId}
handleLeaveSenderRoom={handleLeaveSenderRoom}
@@ -243,7 +236,7 @@ const ClipboardApp = () => {
setRetrieveRoomIdInput={setRetrieveRoomIdInput}
joinRoom={joinRoom}
retrieveJoinRoomBtnRef={retrieveJoinRoomBtnRef}
receiver={receiver}
richTextToPlainText={richTextToPlainText}
handleDownloadFile={handleDownloadFile}
requestFile={requestFile}
@@ -10,7 +10,7 @@ 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 type WebRTC_Recipient from "@/lib/webrtc_Recipient";
import { useFileTransferStore } from "@/stores/fileTransferStore";
interface RetrieveTabPanelProps {
@@ -23,7 +23,6 @@ interface RetrieveTabPanelProps {
setRetrieveRoomIdInput: (value: string) => void;
joinRoom: (isSender: boolean, roomId: string) => void;
retrieveJoinRoomBtnRef: React.RefObject<HTMLButtonElement>;
receiver: WebRTC_Recipient | null;
richTextToPlainText: (html: string) => string;
handleDownloadFile: (meta: FileMeta) => void;
// Functions for WebRTC interaction, passed from parent via useWebRTCConnection
@@ -44,7 +43,6 @@ export function RetrieveTabPanel({
setRetrieveRoomIdInput,
joinRoom,
retrieveJoinRoomBtnRef,
receiver,
richTextToPlainText,
handleDownloadFile,
requestFile,
@@ -117,7 +115,7 @@ export function RetrieveTabPanel({
<div id="retrieve-panel" role="tabpanel" aria-labelledby="retrieve-tab">
<div className="mb-3 text-sm text-gray-600">
{retrieveRoomStatusText ||
(receiver && receiver.isInRoom
(isReceiverInRoom
? messages.text.ClipboardApp.roomStatus.connected_dis
: messages.text.ClipboardApp.roomStatus.receiverEmptyMsg)}
</div>
@@ -141,16 +139,14 @@ export function RetrieveTabPanel({
className="flex-1"
onClick={() => joinRoom(false, retrieveRoomIdInput)}
ref={retrieveJoinRoomBtnRef}
disabled={
!receiver || isReceiverInRoom || !retrieveRoomIdInput.trim()
}
disabled={isReceiverInRoom || !retrieveRoomIdInput.trim()}
>
{messages.text.ClipboardApp.html.joinRoom_dis}
</Button>
<Button
variant="outline"
onClick={handleLeaveRoom}
disabled={!receiver || !isReceiverInRoom || isAnyFileTransferring}
disabled={!isReceiverInRoom || isAnyFileTransferring}
>
{messages.text.ClipboardApp.roomStatus.leaveRoomBtn}
</Button>
@@ -12,7 +12,7 @@ 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 type WebRTC_Initiator from "@/lib/webrtc_Initiator";
import { useFileTransferStore } from "@/stores/fileTransferStore";
// Dynamically import the RichTextEditor
@@ -37,7 +37,6 @@ interface SendTabPanelProps {
processRoomIdInput: (roomId: string) => void; // Passed from useRoomManager
joinRoom: (isSender: boolean, roomId: string) => void;
generateShareLinkAndBroadcast: () => void;
sender: WebRTC_Initiator | null;
shareMessage: string;
currentValidatedShareRoomId: string;
handleLeaveSenderRoom: () => void; // New prop for leaving room
@@ -52,7 +51,6 @@ export function SendTabPanel({
processRoomIdInput,
joinRoom,
generateShareLinkAndBroadcast,
sender,
shareMessage,
currentValidatedShareRoomId,
handleLeaveSenderRoom,
@@ -137,7 +135,7 @@ export function SendTabPanel({
<Button
className="w-full sm:w-auto"
onClick={() => joinRoom(true, inputFieldValue.trim())} // Attempt to join using the current input field value
disabled={!sender || isSenderInRoom || !inputFieldValue.trim()}
disabled={isSenderInRoom || !inputFieldValue.trim()}
>
{messages.text.ClipboardApp.html.joinRoom_dis}
</Button>
@@ -148,7 +146,6 @@ export function SendTabPanel({
onClick={generateShareLinkAndBroadcast}
loadingText={messages.text.ClipboardApp.html.SyncSending_loadingText}
disabled={
!sender ||
!isSenderInRoom ||
(sendFiles.length === 0 && shareContent.trim() === "") ||
!currentValidatedShareRoomId.trim() ||
@@ -160,7 +157,7 @@ export function SendTabPanel({
<Button
variant="outline"
onClick={handleLeaveSenderRoom}
disabled={!sender || !isSenderInRoom || isAnyFileTransferring}
disabled={!isSenderInRoom || isAnyFileTransferring}
>
{messages.text.ClipboardApp.roomStatus.leaveRoomBtn}
</Button>
+195 -295
View File
@@ -1,39 +1,22 @@
import { useEffect, useCallback, useMemo } from "react";
import { useCallback, useEffect } from "react";
import { webrtcService } from "@/lib/webrtcService";
import { useFileTransferStore } from "@/stores/fileTransferStore";
import { fetchRoom, createRoom, checkRoom, leaveRoom } from "@/app/config/api";
import { debounce } from "lodash";
import type { Messages } from "@/types/messages";
import type WebRTC_Initiator from "@/lib/webrtc_Initiator";
import type WebRTC_Recipient from "@/lib/webrtc_Recipient";
import { useFileTransferStore } from "@/stores/fileTransferStore";
function format_peopleMsg(template: string, peerCount: number) {
return template.replace("{peerCount}", peerCount.toString());
}
// 移除所有 WebRTC 相关的 props 依赖
interface UseRoomManagerProps {
messages: Messages | null;
putMessageInMs: (
message: string,
isShareEnd?: boolean,
displayTimeMs?: number
) => void;
sender: WebRTC_Initiator | null;
receiver: WebRTC_Recipient | null;
broadcastDataToPeers: () => Promise<boolean>;
resetSenderConnection: () => Promise<void>;
resetReceiverConnection: () => Promise<void>;
putMessageInMs: (message: string, isShareEnd?: boolean, displayTimeMs?: number) => void;
}
export function useRoomManager({
messages,
putMessageInMs,
sender,
receiver,
broadcastDataToPeers,
resetSenderConnection,
resetReceiverConnection,
}: UseRoomManagerProps) {
// 从 store 中获取状态
export function useRoomManager({ messages, putMessageInMs }: UseRoomManagerProps) {
// 从 store 获取状态
const {
shareRoomId,
initShareRoomId,
@@ -51,295 +34,207 @@ export function useRoomManager({
setShareLink,
setShareRoomStatusText,
setRetrieveRoomStatusText,
setSharePeerCount,
setRetrievePeerCount,
resetReceiverState,
resetSenderApp,
} = useFileTransferStore();
// Receiver leave room function
const handleLeaveReceiverRoom = useCallback(async () => {
console.log(`[RoomManager Debug] Receiver leaving room manually`);
if (!receiver || !receiver.roomId || !receiver.peerId || !messages) return;
try {
await leaveRoom(receiver.roomId, receiver.peerId);
putMessageInMs(messages.text.ClipboardApp.roomStatus.leftRoomMsg, false);
} catch (error) {
console.error("Error leaving room:", error);
putMessageInMs("Failed to leave the room.", false);
} finally {
// Reset application state (不清空房间ID)
resetReceiverState();
// 清理WebRTC连接状态
await resetReceiverConnection();
console.log(
`[RoomManager Debug] Receiver left room and WebRTC connection reset`
);
}
}, [
receiver,
putMessageInMs,
messages,
resetReceiverState,
resetReceiverConnection,
]);
// 加入房间方法 - 直接使用 webrtcService
const joinRoom = useCallback(async (isSenderSide: boolean, roomId: string) => {
if (!messages) return;
// Reset sender app state
try {
console.log(`[RoomManager] 正在加入房间: ${roomId} (${isSenderSide ? '发送方' : '接收方'})`);
// 如果是发送方且房间ID不是初始ID,需要先创建房间
if (isSenderSide && activeTab === "send" && roomId !== initShareRoomId) {
try {
const success = await createRoom(roomId);
if (!success) {
putMessageInMs(messages.text.ClipboardApp.joinRoom.DuplicateMsg, isSenderSide);
return;
}
setShareRoomId(roomId);
} catch (error) {
putMessageInMs(
messages.text.ClipboardApp.joinRoom.failMsg + ` (Create room error)`,
isSenderSide
);
return;
}
}
// 确定实际要加入的房间ID
const actualRoomId = isSenderSide && roomId !== initShareRoomId
? roomId
: isSenderSide
? shareRoomId
: roomId;
// 直接调用 service 方法,无需依赖注入
await webrtcService.joinRoom(actualRoomId, isSenderSide);
putMessageInMs(messages.text.ClipboardApp.joinRoom.successMsg, isSenderSide, 6000);
// 更新分享链接
if (isSenderSide) {
const link = `${window.location.origin}${window.location.pathname}?roomId=${actualRoomId}`;
setShareLink(link);
if (actualRoomId !== shareRoomId) {
setShareRoomId(actualRoomId);
}
}
console.log(`[RoomManager] 成功加入房间: ${actualRoomId}`);
// 注意:广播逻辑已经在 webrtcService 的 onDataChannelOpen 事件中自动处理
} catch (error) {
console.error("[RoomManager] 加入房间失败:", error);
let errorMsg = messages.text.ClipboardApp.joinRoom.failMsg;
if (error instanceof Error) {
errorMsg = error.message === "Room does not exist"
? messages.text.ClipboardApp.joinRoom.notExist
: `${messages.text.ClipboardApp.joinRoom.failMsg} ${error.message}`;
}
putMessageInMs(errorMsg, isSenderSide);
}
}, [messages, putMessageInMs, activeTab, initShareRoomId, shareRoomId, setShareRoomId, setShareLink]);
// 生成分享链接并广播
const generateShareLinkAndBroadcast = useCallback(async () => {
if (!messages || !shareRoomId) return;
try {
if (sharePeerCount === 0) {
putMessageInMs(messages.text.ClipboardApp.waitting_tips, true);
} else {
// 直接调用 service 的广播方法
await webrtcService.broadcastDataToAllPeers();
}
// 更新分享链接
const link = `${window.location.origin}${window.location.pathname}?roomId=${shareRoomId}`;
setShareLink(link);
console.log("[RoomManager] 分享链接已生成并广播");
} catch (error) {
console.error("[RoomManager] 生成分享链接失败:", error);
putMessageInMs("生成分享链接失败", true);
}
}, [messages, putMessageInMs, shareRoomId, sharePeerCount, setShareLink]);
// 接收方离开房间
const handleLeaveReceiverRoom = useCallback(async () => {
if (!messages) return;
try {
console.log("[RoomManager] 接收方离开房间");
// 调用后端 API 离开房间
if (webrtcService.receiver.roomId && webrtcService.receiver.peerId) {
await leaveRoom(webrtcService.receiver.roomId, webrtcService.receiver.peerId);
}
putMessageInMs(messages.text.ClipboardApp.roomStatus.leftRoomMsg, false);
// 重置接收方状态
resetReceiverState();
// 清理 WebRTC 连接
await webrtcService.leaveRoom(false);
console.log("[RoomManager] 接收方已离开房间并重置状态");
} catch (error) {
console.error("[RoomManager] 接收方离开房间失败:", error);
putMessageInMs("离开房间失败", true);
}
}, [messages, putMessageInMs, resetReceiverState]);
// 发送方重置应用状态
const resetSenderAppState = useCallback(async () => {
try {
// 1. Clean up WebRTC connections and reset peer count
await resetSenderConnection();
console.log("[RoomManager] 重置发送方应用状态");
// 1. 清理 WebRTC 连接
await webrtcService.leaveRoom(true);
// 2. Clear share link and progress
// 2. 清除分享链接和进度
resetSenderApp();
// 3. Get new room ID from backend
// 3. 从后端获取新的房间ID
const newRoomId = await fetchRoom();
setShareRoomId(newRoomId || "");
setInitShareRoomId(newRoomId || "");
console.log(
"Sender application state reset successfully, new room ID:",
newRoomId
);
console.log("[RoomManager] 发送方状态重置完成,新房间ID:", newRoomId);
} catch (error) {
console.error("Error during sender state reset:", error);
putMessageInMs("Error resetting sender state.", true);
console.error("[RoomManager] 重置发送方状态失败:", error);
putMessageInMs("重置发送方状态失败", true);
}
}, [
resetSenderConnection,
putMessageInMs,
resetSenderApp,
setShareRoomId,
setInitShareRoomId,
]);
}, [putMessageInMs, resetSenderApp, setShareRoomId, setInitShareRoomId]);
// Sender leave room function
// 发送方离开房间
const handleLeaveSenderRoom = useCallback(async () => {
if (!sender || !sender.roomId || !sender.peerId || !messages) return;
try {
await leaveRoom(sender.roomId, sender.peerId);
putMessageInMs(messages.text.ClipboardApp.roomStatus.leftRoomMsg, true);
} catch (error) {
console.error("Error leaving room:", error);
putMessageInMs("Failed to leave the room.", true);
} finally {
// Reset sender state and get new room ID
await resetSenderAppState();
}
}, [sender, putMessageInMs, resetSenderAppState, messages]);
if (!messages) return;
// Initialize shareRoomId on mount
try {
console.log("[RoomManager] 发送方离开房间");
// 调用后端 API 离开房间
if (webrtcService.sender.roomId && webrtcService.sender.peerId) {
await leaveRoom(webrtcService.sender.roomId, webrtcService.sender.peerId);
}
putMessageInMs(messages.text.ClipboardApp.roomStatus.leftRoomMsg, true);
// 重置发送方状态并获取新房间ID
await resetSenderAppState();
} catch (error) {
console.error("[RoomManager] 发送方离开房间失败:", error);
putMessageInMs("离开房间失败", true);
}
}, [messages, putMessageInMs, resetSenderAppState]);
// 房间ID输入处理
const processRoomIdInput = useCallback(
debounce(async (input: string) => {
if (!input.trim() || !messages) return;
try {
const isValid = await checkRoom(input);
if (isValid) {
console.log(`[RoomManager] 房间 ${input} 验证成功`);
setShareRoomId(input);
putMessageInMs(messages.text.ClipboardApp.roomCheck.available_msg, true);
} else {
putMessageInMs(messages.text.ClipboardApp.roomCheck.notAvailable_msg, true);
}
} catch (error) {
console.error("[RoomManager] 验证房间失败:", error);
putMessageInMs("验证房间失败", true);
}
}, 750),
[messages, putMessageInMs, setShareRoomId]
);
// 初始化发送方房间ID
useEffect(() => {
if (
messages &&
putMessageInMs &&
!initShareRoomId &&
activeTab === "send"
) {
// Ensure this only runs on the sender's side on initial load
if (messages && putMessageInMs && !initShareRoomId && activeTab === "send") {
const initNewRoom = async () => {
try {
const newRoomId = await fetchRoom();
setShareRoomId(newRoomId || "");
setInitShareRoomId(newRoomId || "");
} catch (err) {
console.error("Error fetching initial room:", err);
const errorMsg =
messages.text?.ClipboardApp?.fetchRoom_err ||
"Error fetching room ID.";
console.error("[RoomManager] 获取初始房间失败:", err);
const errorMsg = messages.text?.ClipboardApp?.fetchRoom_err || "获取房间ID失败";
putMessageInMs(errorMsg, true);
}
};
initNewRoom();
}
}, [
messages,
initShareRoomId,
activeTab,
setShareRoomId,
setInitShareRoomId,
putMessageInMs,
]);
}, [messages, putMessageInMs, initShareRoomId, activeTab, setShareRoomId, setInitShareRoomId]);
// Debounced function to actually check the room ID and update the state
const performDebouncedRoomCheck = useMemo(
() =>
debounce(async (roomIdToCheck: string) => {
if (!messages || !putMessageInMs) return;
if (!roomIdToCheck.trim()) {
return;
}
try {
const available = await checkRoom(roomIdToCheck);
if (available) {
setShareRoomId(roomIdToCheck);
putMessageInMs(
messages.text.ClipboardApp.roomCheck.available_msg,
true
);
} else {
putMessageInMs(
messages.text.ClipboardApp.roomCheck.notAvailable_msg,
true
);
}
} catch (error) {
console.error("Error checking room availability:", error);
putMessageInMs("Error checking room.", true);
}
}, 750),
[messages, putMessageInMs, setShareRoomId]
);
// UI calls this function to handle changes in the room ID input
const processRoomIdInput = useCallback(
(inputRoomId: string) => {
if (!inputRoomId.trim() && messages && putMessageInMs) {
putMessageInMs(messages.text.ClipboardApp.roomCheck.empty_msg, true);
performDebouncedRoomCheck.cancel();
return;
}
performDebouncedRoomCheck(inputRoomId);
},
[performDebouncedRoomCheck, messages, putMessageInMs]
);
const joinRoom = useCallback(
async (isSenderSide: boolean, currentRoomIdToJoin: string) => {
if (
!messages ||
!putMessageInMs ||
(isSenderSide && !sender) ||
(!isSenderSide && !receiver)
) {
console.warn("joinRoom prerequisites not met");
return;
}
const peer = isSenderSide ? sender : receiver;
if (!peer) return;
if (!currentRoomIdToJoin.trim()) {
putMessageInMs(
messages.text.ClipboardApp.joinRoom.EmptyMsg,
isSenderSide
);
return;
}
// Create room if sender and not the initial room ID
if (isSenderSide && activeTab === "send" && !peer.isInRoom) {
if (currentRoomIdToJoin !== initShareRoomId) {
try {
const success = await createRoom(currentRoomIdToJoin);
if (!success) {
putMessageInMs(
messages.text.ClipboardApp.joinRoom.DuplicateMsg,
isSenderSide
);
return;
}
setShareRoomId(currentRoomIdToJoin);
} catch (error) {
putMessageInMs(
messages.text.ClipboardApp.joinRoom.failMsg +
` (Create room error)`,
isSenderSide
);
return;
}
}
}
try {
const actualRoomIdForSenderJoin =
isSenderSide && currentRoomIdToJoin !== initShareRoomId
? currentRoomIdToJoin
: isSenderSide
? shareRoomId
: currentRoomIdToJoin;
console.log(
`[RoomManager Debug] ${
isSenderSide ? "Sender" : "Receiver"
} joining room: ${actualRoomIdForSenderJoin}`
);
console.log(
`[RoomManager Debug] Peer current state - isInRoom: ${peer.isInRoom}, roomId: ${peer.roomId}`
);
await peer.joinRoom(actualRoomIdForSenderJoin, isSenderSide);
putMessageInMs(
messages.text.ClipboardApp.joinRoom.successMsg,
isSenderSide,
6000
);
// 更新 Store 中的房间状态
if (isSenderSide) {
useFileTransferStore.getState().setIsSenderInRoom(true);
console.log(
`[RoomManager Debug] Sender joined room, setIsSenderInRoom(true)`
);
const link = `${window.location.origin}${window.location.pathname}?roomId=${actualRoomIdForSenderJoin}`;
setShareLink(link);
if (actualRoomIdForSenderJoin !== shareRoomId) {
setShareRoomId(actualRoomIdForSenderJoin);
}
} else {
useFileTransferStore.getState().setIsReceiverInRoom(true);
console.log(
`[RoomManager Debug] Receiver joined room, setIsReceiverInRoom(true)`
);
}
} catch (error) {
let errorMsgToShow = messages.text.ClipboardApp.joinRoom.failMsg;
if (error instanceof Error) {
errorMsgToShow =
error.message === "Room does not exist"
? messages.text.ClipboardApp.joinRoom.notExist
: `${messages.text.ClipboardApp.joinRoom.failMsg} ${error.message}`;
}
putMessageInMs(errorMsgToShow, isSenderSide);
}
},
[
messages,
putMessageInMs,
sender,
receiver,
activeTab,
initShareRoomId,
shareRoomId,
setShareLink,
setShareRoomId,
]
);
const generateShareLinkAndBroadcast = useCallback(async () => {
if (!sender || !messages || !putMessageInMs || !shareRoomId) return;
if (sharePeerCount === 0) {
putMessageInMs(messages.text.ClipboardApp.waitting_tips, true);
} else {
await broadcastDataToPeers();
}
const link = `${window.location.origin}${window.location.pathname}?roomId=${shareRoomId}`;
setShareLink(link);
}, [
sender,
messages,
putMessageInMs,
shareRoomId,
sharePeerCount,
setShareLink,
broadcastDataToPeers,
]);
// useEffect for room status text
// 房间状态文本更新
useEffect(() => {
if (!messages) {
if (activeTab === "send") setShareRoomStatusText("");
@@ -348,25 +243,22 @@ export function useRoomManager({
}
const isInRoom = activeTab === "send" ? isSenderInRoom : isReceiverInRoom;
const currentPeerCount =
activeTab === "send" ? sharePeerCount : retrievePeerCount;
const currentPeerCount = activeTab === "send" ? sharePeerCount : retrievePeerCount;
let statusText = "";
if (!isInRoom) {
statusText =
activeTab === "retrieve"
? messages.text.ClipboardApp.roomStatus.receiverEmptyMsg
: messages.text.ClipboardApp.roomStatus.senderEmptyMsg;
statusText = activeTab === "retrieve"
? messages.text.ClipboardApp.roomStatus.receiverEmptyMsg
: messages.text.ClipboardApp.roomStatus.senderEmptyMsg;
} else if (currentPeerCount === 0) {
statusText = messages.text.ClipboardApp.roomStatus.onlyOneMsg;
} else {
statusText =
activeTab === "send"
? format_peopleMsg(
messages.text.ClipboardApp.roomStatus.peopleMsg_template,
currentPeerCount + 1
)
: messages.text.ClipboardApp.roomStatus.connected_dis;
statusText = activeTab === "send"
? format_peopleMsg(
messages.text.ClipboardApp.roomStatus.peopleMsg_template,
currentPeerCount + 1
)
: messages.text.ClipboardApp.roomStatus.connected_dis;
}
if (activeTab === "send") setShareRoomStatusText(statusText);
@@ -384,15 +276,23 @@ export function useRoomManager({
]);
return {
// 状态
shareRoomId,
initShareRoomId,
shareLink,
shareRoomStatusText,
retrieveRoomStatusText,
sharePeerCount,
retrievePeerCount,
senderDisconnected,
isSenderInRoom,
isReceiverInRoom,
// 方法
processRoomIdInput,
joinRoom,
generateShareLinkAndBroadcast,
handleLeaveReceiverRoom,
handleLeaveSenderRoom,
};
}
}
+35 -368
View File
@@ -1,410 +1,77 @@
import { useState, useEffect, useCallback, useMemo } from "react";
import WebRTC_Initiator from "@/lib/webrtc_Initiator";
import WebRTC_Recipient from "@/lib/webrtc_Recipient";
import FileSender from "@/lib/fileSender";
import FileReceiver from "@/lib/fileReceiver";
import {
config,
getIceServers,
getSocketOptions,
} from "@/app/config/environment";
import type { CustomFile, fileMetadata, FileMeta } from "@/types/webrtc";
import { useEffect, useMemo } from 'react';
import { webrtcService } from '@/lib/webrtcService';
import { useFileTransferStore } from '@/stores/fileTransferStore';
import type { Messages } from "@/types/messages";
import { useFileTransferStore } from "@/stores/fileTransferStore";
const developmentEnv = process.env.NEXT_PUBLIC_development === "true";
// Types for progress states
// 保留类型定义以保持兼容性
export type PeerProgressDetails = { progress: number; speed: number };
export type FileProgressPeers = { [peerId: string]: PeerProgressDetails };
export type ProgressState = { [fileId: string]: FileProgressPeers };
interface UseWebRTCConnectionProps {
// For user feedback and messages from the hook, if any (mostly for console for now)
messages: Messages | null;
putMessageInMs: (
message: string,
isShareEnd?: boolean,
displayTimeMs?: number
) => void;
putMessageInMs: (message: string, isShareEnd?: boolean, displayTimeMs?: number) => void;
}
export function useWebRTCConnection({
messages,
putMessageInMs,
}: UseWebRTCConnectionProps) {
const [sender, setSender] = useState<WebRTC_Initiator | null>(null);
const [receiver, setReceiver] = useState<WebRTC_Recipient | null>(null);
const [senderFileTransfer, setSenderFileTransfer] =
useState<FileSender | null>(null);
const [receiverFileTransfer, setReceiverFileTransfer] =
useState<FileReceiver | null>(null);
// 从 store 中获取状态和数据
export function useWebRTCConnection({ messages, putMessageInMs }: UseWebRTCConnectionProps) {
// 从 store 获取状态
const {
shareContent,
sendFiles,
sharePeerCount,
retrievePeerCount,
senderDisconnected,
sendProgress,
receiveProgress,
senderDisconnected,
setSharePeerCount,
setRetrievePeerCount,
setSendProgress,
setReceiveProgress,
setSenderDisconnected,
setIsAnyFileTransferring,
} = useFileTransferStore();
// Calculate isAnyFileTransferring internally based on progress states
// 计算是否有文件正在传输
const isAnyFileTransferring = useMemo(() => {
const allProgress = [
...Object.values(sendProgress),
...Object.values(receiveProgress),
];
return allProgress.some((fileProgress: unknown) => {
const typedFileProgress = fileProgress as FileProgressPeers;
return Object.values(typedFileProgress).some((progress: unknown) => {
const typedProgress = progress as PeerProgressDetails;
return typedProgress.progress > 0 && typedProgress.progress < 1;
return allProgress.some((fileProgress: any) => {
return Object.values(fileProgress).some((progress: any) => {
return progress.progress > 0 && progress.progress < 1;
});
});
}, [sendProgress, receiveProgress]);
// 更新 store 中的 isAnyFileTransferring 状态
useEffect(() => {
setIsAnyFileTransferring(isAnyFileTransferring);
}, [isAnyFileTransferring, setIsAnyFileTransferring]);
// Initialize WebRTC objects and their cleanup
// 确保服务在 React 生命周期内被初始化
useEffect(() => {
const webRTCConfig = {
iceServers: getIceServers(),
socketOptions: getSocketOptions() || {},
signalingServer: config.API_URL,
};
const senderConn = new WebRTC_Initiator(webRTCConfig);
const receiverConn = new WebRTC_Recipient(webRTCConfig);
setSender(senderConn);
setReceiver(receiverConn);
const senderFT = new FileSender(senderConn);
const receiverFT = new FileReceiver(receiverConn);
setSenderFileTransfer(senderFT);
setReceiverFileTransfer(receiverFT);
if (developmentEnv)
console.log("WebRTC connection and file transfer instances created");
console.log('[useWebRTCConnection] WebRTC 服务已初始化');
return () => {
if (developmentEnv) console.log("Cleaning up WebRTC instances");
senderConn.cleanUpBeforeExit();
receiverConn.cleanUpBeforeExit();
console.log('[useWebRTCConnection] Hook 清理');
};
}, []);
// Internal function to send text and file metadata to a specific peer
const sendStringAndMetasToPeer = useCallback(
async (peerId: string, textContent: string, filesToSend: CustomFile[]) => {
if (!senderFileTransfer) {
console.error(
"SenderFileTransfer not initialized for sendStringAndMetasToPeer"
);
return;
}
if (textContent) {
await senderFileTransfer.sendString(textContent, peerId);
}
if (filesToSend.length > 0) {
senderFileTransfer.sendFileMeta(filesToSend, peerId);
}
},
[senderFileTransfer]
);
// Exposed function to broadcast data to all connected sender peers
const broadcastDataToAllPeers = useCallback(async () => {
if (!sender || sender.peerConnections.size === 0) {
if (developmentEnv)
console.warn(
"No sender peers to broadcast to, or sender not initialized."
);
return false;
}
if (!senderFileTransfer) {
console.error("senderFileTransfer is not initialized for broadcast.");
return false;
}
const peerIds = Array.from(sender.peerConnections.keys());
if (developmentEnv)
console.log(`Broadcasting to peers: ${peerIds.join(", ")}`);
try {
await Promise.all(
peerIds.map((peerId) =>
sendStringAndMetasToPeer(peerId, shareContent, sendFiles)
)
);
return true;
} catch (error) {
console.error("Error broadcasting data to peers:", error);
return false;
}
}, [
sender,
senderFileTransfer,
sendStringAndMetasToPeer,
shareContent,
sendFiles,
]);
// Setup sender and receiver event handlers
useEffect(() => {
if (sender && senderFileTransfer) {
sender.onConnectionStateChange = (state, peerId) => {
if (developmentEnv)
console.log(`Sender connection state with ${peerId}: ${state}`);
// 更新连接状态
useFileTransferStore.getState().setShareConnectionState(state as any);
setSharePeerCount(sender.peerConnections.size);
if (state === "connected") {
senderFileTransfer.setProgressCallback(
(fileId, progress: number, speed: number) => {
useFileTransferStore
.getState()
.updateSendProgress(fileId, peerId, { progress, speed });
},
peerId
);
}
};
sender.onDataChannelOpen = () => {
// 当数据通道打开时,标记发送方已加入房间
useFileTransferStore.getState().setIsSenderInRoom(true);
broadcastDataToAllPeers();
};
sender.onPeerDisconnected = (peerId) => {
console.log(`[WebRTC Debug] Sender peer ${peerId} disconnected`);
setTimeout(() => {
const newPeerCount = sender.peerConnections.size;
console.log(
`[WebRTC Debug] Sender peer count after disconnect: ${newPeerCount}`
);
setSharePeerCount(newPeerCount);
}, 0);
};
sender.onError = (error) => {
console.error("Sender Error:", error.message, error.context);
putMessageInMs(`Connection error: ${error.message}`, true);
};
}
if (receiver && receiverFileTransfer) {
receiver.onConnectionStateChange = (state, peerId) => {
if (developmentEnv)
console.log(`Receiver connection state with ${peerId}: ${state}`);
// 更新连接状态
useFileTransferStore
.getState()
.setRetrieveConnectionState(state as any);
setRetrievePeerCount(receiver.peerConnections.size);
if (state === "connected") {
receiverFileTransfer.setProgressCallback(
(fileId, progress: number, speed: number) => {
useFileTransferStore
.getState()
.updateReceiveProgress(fileId, peerId, { progress, speed });
}
);
} else if (state === "failed" || state === "disconnected") {
if (isAnyFileTransferring) {
receiverFileTransfer.gracefulShutdown();
}
}
};
receiverFileTransfer.onStringReceived = (data) => {
const peerId = "testId";
if (developmentEnv) console.log(`String received from peer ${peerId}`);
useFileTransferStore.getState().setRetrievedContent(data);
};
receiverFileTransfer.onFileMetaReceived = (meta) => {
const peerId = "testId";
if (developmentEnv)
console.log(
`File meta received from peer ${peerId} for: ${meta.name}`
);
const { type, ...metaWithoutType } = meta;
const store = useFileTransferStore.getState();
// Filter out existing file with same ID and add the new one
const DPrev = store.retrievedFileMetas.filter(
(existingFile) => existingFile.fileId !== metaWithoutType.fileId
);
store.setRetrievedFileMetas([...DPrev, metaWithoutType]);
};
receiverFileTransfer.onFileReceived = async (file) => {
const peerId = "testId"; // This should be dynamic in a multi-peer scenario
if (developmentEnv)
console.log(`File received from peer ${peerId}: ${file.name}`);
// Directly call the store action
useFileTransferStore.getState().addRetrievedFile(file);
};
receiver.onPeerDisconnected = (peerId) => {
console.log(`[WebRTC Debug] Receiver peer ${peerId} disconnected`);
setSenderDisconnected(true);
setRetrievePeerCount(0);
// 注意:接收端断开连接时应该保持在房间状态,除非主动离开
console.log(
`[WebRTC Debug] Receiver peer disconnected, but staying in room`
);
};
receiver.onConnectionEstablished = (peerId) => {
console.log(
`[WebRTC Debug] Receiver connection established with ${peerId}`
);
setSenderDisconnected(false);
useFileTransferStore.getState().setIsReceiverInRoom(true);
console.log(
`[WebRTC Debug] Receiver setIsReceiverInRoom(true) after connection established`
);
};
receiver.onError = (error) => {
console.error("Receiver Error:", error.message, error.context);
putMessageInMs(`Connection error: ${error.message}`, false);
};
}
}, [
sender,
senderFileTransfer,
receiver,
receiverFileTransfer,
putMessageInMs,
broadcastDataToAllPeers,
isAnyFileTransferring,
setSharePeerCount,
setRetrievePeerCount,
setSenderDisconnected,
]);
// Calculate isContentPresent from store data
const isContentPresent = useMemo(() => {
return shareContent !== "" || sendFiles.length > 0;
}, [shareContent, sendFiles]);
// Effect to handle graceful shutdown on page unload
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (isContentPresent || isAnyFileTransferring) {
if (isAnyFileTransferring) {
receiverFileTransfer?.gracefulShutdown();
}
e.preventDefault();
e.returnValue = "";
}
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
};
}, [isContentPresent, isAnyFileTransferring, receiverFileTransfer]);
const requestFile = useCallback(
(fileId: string, peerId?: string) => {
if (!receiverFileTransfer) return;
if (developmentEnv)
console.log(
`Requesting file ${fileId} from peer ${peerId || "default"}`
);
receiverFileTransfer.requestFile(fileId);
},
[receiverFileTransfer]
);
const requestFolder = useCallback(
(folderName: string, peerId?: string) => {
if (!receiverFileTransfer) return;
if (developmentEnv)
console.log(
`Requesting folder ${folderName} from peer ${peerId || "default"}`
);
receiverFileTransfer.requestFolder(folderName);
},
[receiverFileTransfer]
);
const setReceiverDirectoryHandle = useCallback(
async (directoryHandle: FileSystemDirectoryHandle): Promise<void> => {
if (!receiverFileTransfer) return;
if (developmentEnv)
console.log("Setting receiver save directory handle.");
return receiverFileTransfer.setSaveDirectory(directoryHandle);
},
[receiverFileTransfer]
);
const getReceiverSaveType = useCallback(():
| { [fileId: string]: boolean }
| undefined => {
return receiverFileTransfer?.saveType;
}, [receiverFileTransfer]);
// Reset function for receiver connection (for leave room functionality)
const resetReceiverConnection = useCallback(async () => {
if (receiver) {
setSenderDisconnected(false);
setRetrievePeerCount(0);
useFileTransferStore.getState().setIsReceiverInRoom(false);
await receiver.leaveRoomAndCleanup();
}
}, [receiver, setSenderDisconnected, setRetrievePeerCount]);
// Reset function for sender connection (for leave room functionality)
const resetSenderConnection = useCallback(async () => {
if (sender) {
await sender.leaveRoomAndCleanup();
setSharePeerCount(0);
useFileTransferStore.getState().setIsSenderInRoom(false);
}
}, [sender, setSharePeerCount]);
// Manual safe save function
const manualSafeSave = useCallback(() => {
if (receiverFileTransfer) {
receiverFileTransfer.gracefulShutdown();
if (putMessageInMs && messages) {
putMessageInMs(
messages.text.FileListDisplay.safeSaveSuccessMsg,
false,
3000
);
}
}
}, [receiverFileTransfer, putMessageInMs, messages]);
return {
sender,
receiver,
// 状态从 store 获取
sharePeerCount,
retrievePeerCount,
senderDisconnected,
sendProgress,
receiveProgress,
isAnyFileTransferring,
broadcastDataToAllPeers,
requestFile,
requestFolder,
setReceiverDirectoryHandle,
getReceiverSaveType,
senderDisconnected,
resetReceiverConnection,
resetSenderConnection,
manualSafeSave,
// 方法直接从 service 暴露
broadcastDataToAllPeers: webrtcService.broadcastDataToAllPeers.bind(webrtcService),
requestFile: webrtcService.requestFile.bind(webrtcService),
requestFolder: webrtcService.requestFolder.bind(webrtcService),
setReceiverDirectoryHandle: webrtcService.setReceiverDirectoryHandle.bind(webrtcService),
getReceiverSaveType: webrtcService.getReceiverSaveType.bind(webrtcService),
manualSafeSave: webrtcService.manualSafeSave.bind(webrtcService),
// 重置连接方法
resetSenderConnection: () => webrtcService.leaveRoom(true),
resetReceiverConnection: () => webrtcService.leaveRoom(false),
// 为了兼容性,保留这些属性(但实际上不再需要)
sender: webrtcService.sender,
receiver: webrtcService.receiver,
};
}
}
+209
View File
@@ -0,0 +1,209 @@
import WebRTC_Initiator from "@/lib/webrtc_Initiator";
import WebRTC_Recipient from "@/lib/webrtc_Recipient";
import FileSender from "@/lib/fileSender";
import FileReceiver from "@/lib/fileReceiver";
import { getIceServers, getSocketOptions, config } from "@/app/config/environment";
import { useFileTransferStore } from "@/stores/fileTransferStore";
import type { CustomFile } from "@/types/webrtc";
class WebRTCService {
public sender: WebRTC_Initiator;
public receiver: WebRTC_Recipient;
public fileSender: FileSender;
public fileReceiver: FileReceiver;
private static instance: WebRTCService;
private constructor() {
const webRTCConfig = {
iceServers: getIceServers(),
socketOptions: getSocketOptions() || {},
signalingServer: config.API_URL,
};
this.sender = new WebRTC_Initiator(webRTCConfig);
this.receiver = new WebRTC_Recipient(webRTCConfig);
this.fileSender = new FileSender(this.sender);
this.fileReceiver = new FileReceiver(this.receiver);
this.initializeEventHandlers();
console.log("WebRTC Service 初始化完成 (单例模式)");
}
public static getInstance(): WebRTCService {
if (!WebRTCService.instance) {
WebRTCService.instance = new WebRTCService();
}
return WebRTCService.instance;
}
private initializeEventHandlers(): void {
// 发送方事件处理
this.sender.onConnectionStateChange = (state, peerId) => {
console.log(`[WebRTC Service] 发送方连接状态: ${state} (对等端: ${peerId})`);
useFileTransferStore.getState().setShareConnectionState(state as any);
useFileTransferStore.getState().setSharePeerCount(this.sender.peerConnections.size);
if (state === 'connected') {
this.fileSender.setProgressCallback((fileId, progress, speed) => {
useFileTransferStore.getState().updateSendProgress(fileId, peerId, { progress, speed });
}, peerId);
}
};
this.sender.onDataChannelOpen = (peerId) => {
console.log(`[WebRTC Service] 发送方数据通道打开: ${peerId}`);
useFileTransferStore.getState().setIsSenderInRoom(true);
// 自动广播当前内容
this.broadcastDataToAllPeers();
};
this.sender.onPeerDisconnected = (peerId) => {
console.log(`[WebRTC Service] 发送方对等端断开: ${peerId}`);
setTimeout(() => {
useFileTransferStore.getState().setSharePeerCount(this.sender.peerConnections.size);
}, 0);
};
this.sender.onError = (error) => {
console.error("[WebRTC Service] 发送方错误:", error.message);
};
// 接收方事件处理
this.receiver.onConnectionStateChange = (state, peerId) => {
console.log(`[WebRTC Service] 接收方连接状态: ${state} (对等端: ${peerId})`);
useFileTransferStore.getState().setRetrieveConnectionState(state as any);
useFileTransferStore.getState().setRetrievePeerCount(this.receiver.peerConnections.size);
if (state === 'connected') {
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();
}
}
};
this.receiver.onConnectionEstablished = (peerId) => {
console.log(`[WebRTC Service] 接收方连接建立: ${peerId}`);
useFileTransferStore.getState().setSenderDisconnected(false);
useFileTransferStore.getState().setIsReceiverInRoom(true);
};
this.receiver.onPeerDisconnected = (peerId) => {
console.log(`[WebRTC Service] 接收方对等端断开: ${peerId}`);
useFileTransferStore.getState().setSenderDisconnected(true);
useFileTransferStore.getState().setRetrievePeerCount(0);
};
this.fileReceiver.onStringReceived = (data) => {
useFileTransferStore.getState().setRetrievedContent(data);
};
this.fileReceiver.onFileMetaReceived = (meta) => {
const { type, ...metaWithoutType } = meta;
const store = useFileTransferStore.getState();
const filteredMetas = store.retrievedFileMetas.filter(
(existingFile) => existingFile.fileId !== metaWithoutType.fileId
);
store.setRetrievedFileMetas([...filteredMetas, metaWithoutType]);
};
this.fileReceiver.onFileReceived = async (file) => {
useFileTransferStore.getState().addRetrievedFile(file);
};
}
// 业务方法
public async joinRoom(roomId: string, isSender: boolean): Promise<void> {
console.log(`[WebRTC Service] 加入房间: ${roomId} (${isSender ? '发送方' : '接收方'})`);
const peer = isSender ? this.sender : this.receiver;
await peer.joinRoom(roomId, isSender);
const setInRoom = isSender
? useFileTransferStore.getState().setIsSenderInRoom
: useFileTransferStore.getState().setIsReceiverInRoom;
setInRoom(true);
}
public async leaveRoom(isSender: boolean): Promise<void> {
console.log(`[WebRTC Service] 离开房间 (${isSender ? '发送方' : '接收方'})`);
if (isSender) {
await this.sender.leaveRoomAndCleanup();
useFileTransferStore.getState().setIsSenderInRoom(false);
useFileTransferStore.getState().setSharePeerCount(0);
} else {
await this.receiver.leaveRoomAndCleanup();
useFileTransferStore.getState().setIsReceiverInRoom(false);
useFileTransferStore.getState().setRetrievePeerCount(0);
}
}
public async broadcastDataToAllPeers(): Promise<boolean> {
const { shareContent, sendFiles } = useFileTransferStore.getState();
const peerIds = Array.from(this.sender.peerConnections.keys());
if (peerIds.length === 0) {
console.warn("[WebRTC Service] 没有连接的对等端进行广播");
return false;
}
try {
await Promise.all(
peerIds.map(async (peerId) => {
if (shareContent) {
await this.fileSender.sendString(shareContent, peerId);
}
if (sendFiles.length > 0) {
this.fileSender.sendFileMeta(sendFiles, peerId);
}
})
);
console.log(`[WebRTC Service] 成功广播到 ${peerIds.length} 个对等端`);
return true;
} catch (error) {
console.error("[WebRTC Service] 广播失败:", error);
return false;
}
}
public requestFile(fileId: string): void {
this.fileReceiver.requestFile(fileId);
}
public requestFolder(folderName: string): void {
this.fileReceiver.requestFolder(folderName);
}
public async setReceiverDirectoryHandle(directoryHandle: FileSystemDirectoryHandle): Promise<void> {
return this.fileReceiver.setSaveDirectory(directoryHandle);
}
public getReceiverSaveType(): { [fileId: string]: boolean } | undefined {
return this.fileReceiver.saveType;
}
public manualSafeSave(): void {
this.fileReceiver.gracefulShutdown();
}
public async cleanup(): Promise<void> {
console.log("[WebRTC Service] 开始清理...");
try {
await Promise.all([
this.sender.cleanUpBeforeExit(),
this.receiver.cleanUpBeforeExit()
]);
console.log("[WebRTC Service] 清理完成");
} catch (error) {
console.error("[WebRTC Service] 清理过程中出错:", error);
}
}
}
export const webrtcService = WebRTCService.getInstance();