refactor(chat): move WebSocket lifecycle into chat machine via fromCallback actor
- chat-screen no longer manages WebSocket directly; it now dispatches three auth lifecycle events (ChatGuestLogin, ChatNonGuestLogin, ChatLogout) derived from auth.loginStatus - chat-machine internally owns the WebSocket long-lived actor via fromCallback, completing the previously deferred migration scope - removed ChatWebSocket import, getWsToken helper, and direct WS effect from chat-screen; renamed dispatchInitialLoad to dispatchAuthLifecycle to reflect the new responsibility - decouples auth from chat machine via event-driven boundary: machine stays unaware of loginStatus, guest semantics live at the dispatch layer (guest → skip WS, non-guest → connect with token)
This commit is contained in:
@@ -5,13 +5,10 @@
|
||||
* 原始 Dart: lib/ui/chat/chat_screen.dart(172 行)
|
||||
*
|
||||
* 职责(本文件):
|
||||
* - **订阅 auth 状态机**(loginStatus)——
|
||||
* - 派生 isGuest / 驱动 WS 生命周期 / 驱动初次加载(按 loginStatus 选 ChatInit vs ChatLoadMoreHistory)
|
||||
* - 驱动历史加载
|
||||
* - 屏幕初始化(mount 时按 loginStatus 派初次加载)
|
||||
* - **订阅 auth 状态机**(loginStatus)—— 派生 isGuest / **派鉴权生命周期事件**给 chat 机器
|
||||
* - 屏幕生命周期事件(init / visible / invisible)
|
||||
* - 配额告警监听(**仅**游客 —— "非游客无消息限制"业务事实)
|
||||
* - WebSocket 生命周期管理(基于 auth 状态机的 loginStatus)
|
||||
* - 渲染骨架(背景 + 顶部栏 + 消息区 + 输入栏)
|
||||
* - **不**直接管理 WebSocket / **不**直接读 AuthStorage(除 token 取值)
|
||||
*
|
||||
* 子组件职责:
|
||||
* - ChatHeader:顶部栏(游客 banner / 菜单按钮)
|
||||
@@ -22,13 +19,13 @@
|
||||
* - BrowserHintOverlay:浏览器提示
|
||||
* - CharacterIntro:角色介绍卡片(首屏)
|
||||
*
|
||||
* **鉴权解耦**:
|
||||
* chat 机器**不**感知鉴权 —— isGuest 在此派生自 `auth.loginStatus === "guest"`。
|
||||
* chat 机器**不**管 WebSocket 生命周期 —— 由此 useEffect 监听 loginStatus 驱动。
|
||||
*
|
||||
* **配额(**仅**游客)**:
|
||||
* - 游客:派 ChatInit → readInitData 调 ChatStorage → onDone 条件赋
|
||||
* - 非游客:派 ChatLoadMoreHistory(无配额 init)—— "非游客无消息限制"业务事实
|
||||
* **鉴权解耦**(**事件驱动**):
|
||||
* - chat 机器**不**感知鉴权 / **不**管 WebSocket —— 由 chat-screen 派生 loginStatus
|
||||
* - chat-screen **只**派 3 个事件:
|
||||
* - `ChatGuestLogin` → 游客会话(**断** WS = 不连)
|
||||
* - `ChatNonGuestLogin { token }` → 非游客会话(**连** WS)
|
||||
* - `ChatLogout` → 登出(机器自动 cleanup WS actor)
|
||||
* - chat 机器**内部**用 fromCallback actor 完整管理 WS 生命周期
|
||||
*/
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Image from "next/image";
|
||||
@@ -38,10 +35,6 @@ import type { LoginStatus } from "@/models/auth/login-status";
|
||||
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
||||
import { useChatDispatch, useChatState } from "@/stores/chat/chat-context";
|
||||
import { GuestChatQuota } from "@/stores/chat";
|
||||
import {
|
||||
ChatWebSocket,
|
||||
createChatWebSocket,
|
||||
} from "@/core/net/chat-websocket";
|
||||
|
||||
import { MobileShell } from "@/app/_components/core/mobile-shell";
|
||||
|
||||
@@ -61,34 +54,31 @@ function deriveIsGuest(loginStatus: LoginStatus): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* 拿 WebSocket token —— 按 loginStatus 选 loginToken 或 guestToken
|
||||
* 按 loginStatus 派鉴权生命周期事件(chat 机器**不**感知 loginStatus —— 这层转换在此)
|
||||
*
|
||||
* 设计:**不**直接管理 WS / **不**读 AuthStorage(除取 token)
|
||||
*/
|
||||
async function getWsToken(
|
||||
loginStatus: LoginStatus,
|
||||
): Promise<string | null> {
|
||||
if (loginStatus === "notLoggedIn") return null;
|
||||
const storage = AuthStorage.getInstance();
|
||||
if (loginStatus === "guest") {
|
||||
const r = await storage.getGuestToken();
|
||||
return r.success ? r.data : null;
|
||||
}
|
||||
// email / google / facebook / apple:都用业务 loginToken(OAuth sync 写下的)
|
||||
const r = await storage.getLoginToken();
|
||||
return r.success ? r.data : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 loginStatus 派初次加载事件(**仅**游客派 ChatInit —— 配额 init)
|
||||
*/
|
||||
function dispatchInitialLoad(
|
||||
function dispatchAuthLifecycle(
|
||||
loginStatus: LoginStatus,
|
||||
chatDispatch: ReturnType<typeof useChatDispatch>,
|
||||
): void {
|
||||
if (loginStatus === "guest") {
|
||||
chatDispatch({ type: "ChatInit" }); // 拉本地消息 + 初始化配额
|
||||
} else if (loginStatus !== "notLoggedIn") {
|
||||
chatDispatch({ type: "ChatLoadMoreHistory" }); // 拉服务器端首屏(**无**配额 init)
|
||||
// 游客:派 ChatGuestLogin(chat 机器**不**连 WS —— 业务事实"断 WS")
|
||||
chatDispatch({ type: "ChatGuestLogin" });
|
||||
return;
|
||||
}
|
||||
if (loginStatus === "notLoggedIn") {
|
||||
// 登出:派 ChatLogout(chat 机器自动 cleanup WS actor + 清消息)
|
||||
chatDispatch({ type: "ChatLogout" });
|
||||
return;
|
||||
}
|
||||
// 非游客:拿 loginToken → 派 ChatNonGuestLogin { token }
|
||||
void (async () => {
|
||||
const tokenR = await AuthStorage.getInstance().getLoginToken();
|
||||
if (tokenR.success && tokenR.data) {
|
||||
chatDispatch({ type: "ChatNonGuestLogin", token: tokenR.data });
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
export function ChatScreen() {
|
||||
@@ -103,92 +93,35 @@ export function ChatScreen() {
|
||||
const [quotaExhausted, setQuotaExhausted] = useState(false);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// mount 时按 loginStatus 派初次加载
|
||||
// 屏幕生命周期 + 派初次鉴权生命周期事件
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
dispatchInitialLoad(authState.loginStatus, chatDispatch);
|
||||
chatDispatch({ type: "ChatScreenVisible" });
|
||||
dispatchAuthLifecycle(authState.loginStatus, chatDispatch);
|
||||
return () => {
|
||||
chatDispatch({ type: "ChatScreenInvisible" });
|
||||
};
|
||||
// 注:mount useEffect 故意**不**依赖 loginStatus —— 首次 mount 时机是固定的
|
||||
// ("用户进 /chat 那一刻"),loginStatus 由 splash/auth-screen 提前 settle。
|
||||
// loginStatus 后续变化由下方"副作用 ②" 统一处理。
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [chatDispatch]);
|
||||
// 注:mount useEffect 故意**不**依赖 loginStatus —— 首次 mount 时机是固定的
|
||||
// ("用户进 /chat 那一刻"),loginStatus 由 splash/auth-screen 提前 settle。
|
||||
// loginStatus 后续变化由下方"副作用 ②" 统一处理。
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 核心副作用 ①:loginStatus 变化 → 连/断 WebSocket
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const wsRef = useRef<ChatWebSocket | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
void (async () => {
|
||||
if (authState.loginStatus === "notLoggedIn") {
|
||||
// 登出 / 未登录:断开 WS
|
||||
if (wsRef.current) {
|
||||
wsRef.current.disconnect();
|
||||
wsRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 已登录:拿 token → 连 WS
|
||||
const token = await getWsToken(authState.loginStatus);
|
||||
if (cancelled || !token) return;
|
||||
|
||||
// 清理旧连接(如有)
|
||||
if (wsRef.current) {
|
||||
wsRef.current.disconnect();
|
||||
wsRef.current = null;
|
||||
}
|
||||
|
||||
const ws = createChatWebSocket(token);
|
||||
ws.onConnected = (userId) => {
|
||||
chatDispatch({ type: "ChatWebSocketConnected", userId });
|
||||
};
|
||||
ws.onSentence = (p) => {
|
||||
chatDispatch({
|
||||
type: "ChatAISentenceReceived",
|
||||
index: p.index,
|
||||
text: p.text,
|
||||
total: p.total,
|
||||
done: p.done,
|
||||
});
|
||||
};
|
||||
ws.onError = (msg) => {
|
||||
chatDispatch({ type: "ChatWebSocketError", errorMessage: msg });
|
||||
};
|
||||
ws.connect();
|
||||
wsRef.current = ws;
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (wsRef.current) {
|
||||
wsRef.current.disconnect();
|
||||
wsRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [authState.loginStatus, chatDispatch]);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 副作用 ②:loginStatus 变化 → 重新加载
|
||||
// 副作用 ②:loginStatus 变化 → 派对应鉴权生命周期事件
|
||||
//
|
||||
// 规则:
|
||||
// - 首次 mount(prev === null)→ 由 mount useEffect 统一处理
|
||||
// - "notLoggedIn" → 不加载
|
||||
// - 登录态变化(guest→email 等)→ 重新加载
|
||||
// - 游客派 ChatInit(拉本地 + **初始化配额**)
|
||||
// - 非游客派 ChatLoadMoreHistory(拉服务器端首屏,**无**配额 init)
|
||||
// - "notLoggedIn" → 派 ChatLogout
|
||||
// - 登录态变化(guest→email 等)→ 派 ChatGuestLogin 或 ChatNonGuestLogin
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const prevLoginStatusRef = useRef<LoginStatus | null>(null);
|
||||
useEffect(() => {
|
||||
const prev = prevLoginStatusRef.current;
|
||||
prevLoginStatusRef.current = authState.loginStatus;
|
||||
if (prev === null) return; // 首次 mount 由 mount useEffect 处理
|
||||
if (authState.loginStatus === "notLoggedIn") return;
|
||||
if (prev !== authState.loginStatus) {
|
||||
dispatchInitialLoad(authState.loginStatus, chatDispatch);
|
||||
}
|
||||
if (prev === authState.loginStatus) return; // 登录态**没**变
|
||||
dispatchAuthLifecycle(authState.loginStatus, chatDispatch);
|
||||
}, [authState.loginStatus, chatDispatch]);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user