refactor(auth): centralize login status sync
This commit is contained in:
@@ -9,7 +9,6 @@ import { useRouter } from "next/navigation";
|
|||||||
|
|
||||||
import { MobileShell } from "@/app/_components/core";
|
import { MobileShell } from "@/app/_components/core";
|
||||||
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
||||||
import { useUserDispatch } from "@/stores/user/user-context";
|
|
||||||
import { ROUTES } from "@/router/routes";
|
import { ROUTES } from "@/router/routes";
|
||||||
|
|
||||||
import { AuthBackground, AuthPanel } from "./components";
|
import { AuthBackground, AuthPanel } from "./components";
|
||||||
@@ -18,10 +17,9 @@ import styles from "./components/auth-screen.module.css";
|
|||||||
export function AuthScreen() {
|
export function AuthScreen() {
|
||||||
const state = useAuthState();
|
const state = useAuthState();
|
||||||
const authDispatch = useAuthDispatch();
|
const authDispatch = useAuthDispatch();
|
||||||
const userDispatch = useUserDispatch();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// 邮箱登录成功 → 通知 User + 跳转
|
// 邮箱登录成功 → 跳转;User hydrate 由根级 <UserAuthSync /> 监听 loginStatus 变化处理。
|
||||||
// 用 pendingRedirect flag(不是 useRef transition detection)——
|
// 用 pendingRedirect flag(不是 useRef transition detection)——
|
||||||
// re-mount 时 flag 在 auth context 里持久,区分"刚按了"和"re-visit"准确无误。
|
// re-mount 时 flag 在 auth context 里持久,区分"刚按了"和"re-visit"准确无误。
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -37,16 +35,9 @@ export function AuthScreen() {
|
|||||||
"[auth-screen] useEffect → router.replace(/chat) [via pendingRedirect flag]",
|
"[auth-screen] useEffect → router.replace(/chat) [via pendingRedirect flag]",
|
||||||
);
|
);
|
||||||
authDispatch({ type: "AuthClearPendingRedirect" });
|
authDispatch({ type: "AuthClearPendingRedirect" });
|
||||||
userDispatch({ type: "UserInit" });
|
|
||||||
router.replace(ROUTES.chat);
|
router.replace(ROUTES.chat);
|
||||||
}
|
}
|
||||||
}, [
|
}, [state.loginStatus, state.pendingRedirect, authDispatch, router]);
|
||||||
state.loginStatus,
|
|
||||||
state.pendingRedirect,
|
|
||||||
authDispatch,
|
|
||||||
userDispatch,
|
|
||||||
router,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobileShell>
|
<MobileShell>
|
||||||
|
|||||||
@@ -10,22 +10,18 @@
|
|||||||
* - BrowserHintOverlay:浏览器提示
|
* - BrowserHintOverlay:浏览器提示
|
||||||
*
|
*
|
||||||
* 鉴权解耦(事件驱动):
|
* 鉴权解耦(事件驱动):
|
||||||
* - chat 机器不感知鉴权 / 不管 WebSocket —— 由 chat-screen 派生 loginStatus
|
* - chat 机器不感知鉴权 / 不管 WebSocket
|
||||||
* - chat-screen 只派 3 个事件:
|
* - auth → chat 的生命周期同步由根级 <ChatAuthSync /> 负责
|
||||||
* - `ChatGuestLogin` → 游客会话(断 WS = 不连)
|
* - ChatScreen 只派生 UI 展示所需的 isGuest
|
||||||
* - `ChatNonGuestLogin { token }` → 非游客会话(连 WS)
|
|
||||||
* - `ChatLogout` → 登出(机器自动 cleanup WS actor)
|
|
||||||
* - chat 机器内部用 fromCallback actor 完整管理 WS 生命周期
|
|
||||||
*/
|
*/
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
import { useAuthState } from "@/stores/auth/auth-context";
|
import { useAuthState } from "@/stores/auth/auth-context";
|
||||||
import type { LoginStatus } from "@/data/dto/auth";
|
import type { LoginStatus } from "@/data/dto/auth";
|
||||||
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
||||||
import { UserStorage } from "@/data/storage/user/user_storage";
|
import { UserStorage } from "@/data/storage/user/user_storage";
|
||||||
import { useChatDispatch, useChatState } from "@/stores/chat/chat-context";
|
import { useChatState } from "@/stores/chat/chat-context";
|
||||||
import { AppEnvUtil, BrowserDetector, UrlLauncherUtil } from "@/utils";
|
import { AppEnvUtil, BrowserDetector, UrlLauncherUtil } from "@/utils";
|
||||||
import { ROUTE_BUILDERS, ROUTES } from "@/router/routes";
|
import { ROUTE_BUILDERS, ROUTES } from "@/router/routes";
|
||||||
|
|
||||||
@@ -50,39 +46,9 @@ function deriveIsGuest(loginStatus: LoginStatus): boolean {
|
|||||||
return loginStatus === "guest";
|
return loginStatus === "guest";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 按 loginStatus 派鉴权生命周期事件(chat 机器不感知 loginStatus —— 这层转换在此)
|
|
||||||
*
|
|
||||||
* 设计:不直接管理 WS / 不读 AuthStorage(除取 token)
|
|
||||||
*/
|
|
||||||
function dispatchAuthLifecycle(
|
|
||||||
loginStatus: LoginStatus,
|
|
||||||
chatDispatch: ReturnType<typeof useChatDispatch>,
|
|
||||||
): void {
|
|
||||||
if (loginStatus === "guest") {
|
|
||||||
// 游客:派 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() {
|
export function ChatScreen() {
|
||||||
const router = useRouter();
|
|
||||||
const state = useChatState();
|
const state = useChatState();
|
||||||
const authState = useAuthState();
|
const authState = useAuthState();
|
||||||
const chatDispatch = useChatDispatch();
|
|
||||||
const [showExternalBrowserDialog, setShowExternalBrowserDialog] =
|
const [showExternalBrowserDialog, setShowExternalBrowserDialog] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
@@ -98,37 +64,7 @@ export function ChatScreen() {
|
|||||||
const showExhaustedBanner =
|
const showExhaustedBanner =
|
||||||
isGuest && state.quotaLoaded && state.quotaExceededTrigger > 0;
|
isGuest && state.quotaLoaded && state.quotaExceededTrigger > 0;
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
// loginStatus 变化 → 派对应鉴权生命周期事件
|
|
||||||
//
|
|
||||||
// 规则:
|
|
||||||
// - AuthInit 未完成 → 等,避免初始 notLoggedIn 把 /chat 变成死状态
|
|
||||||
// - AuthInit 完成后仍 "notLoggedIn" → 清 chat 并回 splash
|
|
||||||
// - 登录态变化(guest→email 等)→ 派 ChatGuestLogin 或 ChatNonGuestLogin
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
const prevLoginStatusRef = useRef<LoginStatus | null>(null);
|
|
||||||
const externalBrowserPromptShownRef = useRef(false);
|
const externalBrowserPromptShownRef = useRef(false);
|
||||||
useEffect(() => {
|
|
||||||
if (!authState.hasInitialized || authState.isLoading) return;
|
|
||||||
|
|
||||||
const prev = prevLoginStatusRef.current;
|
|
||||||
if (prev === authState.loginStatus) return; // 登录态没变
|
|
||||||
prevLoginStatusRef.current = authState.loginStatus;
|
|
||||||
|
|
||||||
if (authState.loginStatus === "notLoggedIn") {
|
|
||||||
chatDispatch({ type: "ChatLogout" });
|
|
||||||
router.replace(ROUTES.splash);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatchAuthLifecycle(authState.loginStatus, chatDispatch);
|
|
||||||
}, [
|
|
||||||
authState.hasInitialized,
|
|
||||||
authState.isLoading,
|
|
||||||
authState.loginStatus,
|
|
||||||
chatDispatch,
|
|
||||||
router,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authState.hasInitialized || authState.isLoading) return;
|
if (!authState.hasInitialized || authState.isLoading) return;
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
|
|
||||||
import { MobileShell, SettingsSection } from "@/app/_components/core";
|
import { MobileShell, SettingsSection } from "@/app/_components/core";
|
||||||
import { ROUTES } from "@/router/routes";
|
|
||||||
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
||||||
import { useUserDispatch, useUserState } from "@/stores/user/user-context";
|
import { useUserDispatch, useUserState } from "@/stores/user/user-context";
|
||||||
|
|
||||||
@@ -36,22 +33,8 @@ export function SidebarScreen() {
|
|||||||
const userDispatch = useUserDispatch();
|
const userDispatch = useUserDispatch();
|
||||||
const auth = useAuthState();
|
const auth = useAuthState();
|
||||||
const authDispatch = useAuthDispatch();
|
const authDispatch = useAuthDispatch();
|
||||||
const router = useRouter();
|
|
||||||
const isLogoutRedirectPendingRef = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
userDispatch({ type: "UserInit" });
|
|
||||||
}, [userDispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLogoutRedirectPendingRef.current) return;
|
|
||||||
if (auth.isLoading || auth.loginStatus !== "notLoggedIn") return;
|
|
||||||
isLogoutRedirectPendingRef.current = false;
|
|
||||||
router.replace(ROUTES.splash);
|
|
||||||
}, [auth.isLoading, auth.loginStatus, router]);
|
|
||||||
|
|
||||||
const handleLogoutClick = () => {
|
const handleLogoutClick = () => {
|
||||||
isLogoutRedirectPendingRef.current = true;
|
|
||||||
userDispatch({ type: "UserClearLocal" });
|
userDispatch({ type: "UserClearLocal" });
|
||||||
authDispatch({ type: "AuthLogoutSubmitted" });
|
authDispatch({ type: "AuthLogoutSubmitted" });
|
||||||
void signOut({ redirect: false });
|
void signOut({ redirect: false });
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useRouter } from "next/navigation";
|
|||||||
|
|
||||||
import { MobileShell } from "@/app/_components/core";
|
import { MobileShell } from "@/app/_components/core";
|
||||||
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
||||||
import { useUserDispatch } from "@/stores/user/user-context";
|
|
||||||
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
||||||
import { ROUTES } from "@/router/routes";
|
import { ROUTES } from "@/router/routes";
|
||||||
import { pwaUtil } from "@/utils";
|
import { pwaUtil } from "@/utils";
|
||||||
@@ -27,7 +26,6 @@ export function SplashScreen() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const state = useAuthState();
|
const state = useAuthState();
|
||||||
const authDispatch = useAuthDispatch();
|
const authDispatch = useAuthDispatch();
|
||||||
const userDispatch = useUserDispatch();
|
|
||||||
|
|
||||||
const handleSkip = () => {
|
const handleSkip = () => {
|
||||||
authDispatch({ type: "AuthGuestLoginSubmitted" });
|
authDispatch({ type: "AuthGuestLoginSubmitted" });
|
||||||
@@ -81,16 +79,9 @@ export function SplashScreen() {
|
|||||||
"[splash] useEffect ② → router.replace(/chat) [via pendingRedirect flag]",
|
"[splash] useEffect ② → router.replace(/chat) [via pendingRedirect flag]",
|
||||||
);
|
);
|
||||||
authDispatch({ type: "AuthClearPendingRedirect" });
|
authDispatch({ type: "AuthClearPendingRedirect" });
|
||||||
userDispatch({ type: "UserInit" });
|
|
||||||
router.replace(ROUTES.chat);
|
router.replace(ROUTES.chat);
|
||||||
}
|
}
|
||||||
}, [
|
}, [state.loginStatus, state.pendingRedirect, authDispatch, router]);
|
||||||
state.loginStatus,
|
|
||||||
state.pendingRedirect,
|
|
||||||
authDispatch,
|
|
||||||
userDispatch,
|
|
||||||
router,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobileShell background="var(--color-sidebar-background)">
|
<MobileShell background="var(--color-sidebar-background)">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
* + AuthStatusChecker(启动时一次:派发 AuthInit)
|
* + AuthStatusChecker(启动时一次:派发 AuthInit)
|
||||||
* + OAuthSessionSync (持续监听 NextAuth session → auth machine)
|
* + OAuthSessionSync (持续监听 NextAuth session → auth machine)
|
||||||
* + UserAuthSync (auth 登录态恢复后 hydrate user machine)
|
* + UserAuthSync (auth 登录态恢复后 hydrate user machine)
|
||||||
|
* + ChatAuthSync (持续监听 auth loginStatus → chat machine)
|
||||||
*
|
*
|
||||||
* 它们始终挂载,保证各页面直接 `use*State()` / `use*Dispatch()` 即可,
|
* 它们始终挂载,保证各页面直接 `use*State()` / `use*Dispatch()` 即可,
|
||||||
* 无需在每个页面单独包裹。
|
* 无需在每个页面单独包裹。
|
||||||
@@ -18,6 +19,7 @@ import type { ReactNode } from "react";
|
|||||||
import { AuthProvider } from "@/stores/auth/auth-context";
|
import { AuthProvider } from "@/stores/auth/auth-context";
|
||||||
import { AuthStatusChecker } from "@/stores/auth/auth-status-checker";
|
import { AuthStatusChecker } from "@/stores/auth/auth-status-checker";
|
||||||
import { OAuthSessionSync } from "@/stores/auth/oauth-session-sync";
|
import { OAuthSessionSync } from "@/stores/auth/oauth-session-sync";
|
||||||
|
import { ChatAuthSync } from "@/stores/chat/chat-auth-sync";
|
||||||
import { ChatProvider } from "@/stores/chat/chat-context";
|
import { ChatProvider } from "@/stores/chat/chat-context";
|
||||||
import { SidebarProvider } from "@/stores/sidebar/sidebar-context";
|
import { SidebarProvider } from "@/stores/sidebar/sidebar-context";
|
||||||
import { UserAuthSync } from "@/stores/user/user-auth-sync";
|
import { UserAuthSync } from "@/stores/user/user-auth-sync";
|
||||||
@@ -39,6 +41,7 @@ export function RootProviders({ children }: RootProvidersProps) {
|
|||||||
<UserAuthSync />
|
<UserAuthSync />
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<ChatProvider>
|
<ChatProvider>
|
||||||
|
<ChatAuthSync />
|
||||||
{children}
|
{children}
|
||||||
<div id="toast-portal" />
|
<div id="toast-portal" />
|
||||||
</ChatProvider>
|
</ChatProvider>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export interface AuthState {
|
|||||||
username: string;
|
username: string;
|
||||||
confirmPassword: string;
|
confirmPassword: string;
|
||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
/** 当前登录状态(未登录 / 游客 / OAuth / 邮箱) */
|
/** 当前登录状态(未登录 / 游客 / OAuth / 邮箱)—— 下游同步器监听此字段的双向变化 */
|
||||||
loginStatus: LoginStatus;
|
loginStatus: LoginStatus;
|
||||||
/** 启动时的本地登录态恢复是否已经完成 */
|
/** 启动时的本地登录态恢复是否已经完成 */
|
||||||
hasInitialized: boolean;
|
hasInitialized: boolean;
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth → Chat 同步器
|
||||||
|
*
|
||||||
|
* Chat machine 不直接感知 Auth machine;这里作为根级桥接器,持续监听
|
||||||
|
* loginStatus 变化并派发 chat 鉴权生命周期事件。这样页面组件只负责 UI,
|
||||||
|
* 不再承担全局状态同步职责。
|
||||||
|
*/
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
import type { LoginStatus } from "@/data/dto/auth";
|
||||||
|
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
||||||
|
import { useAuthState } from "@/stores/auth/auth-context";
|
||||||
|
import { PROTECTED_ROUTES, ROUTES, type StaticRoute } from "@/router/routes";
|
||||||
|
|
||||||
|
import { useChatDispatch } from "./chat-context";
|
||||||
|
|
||||||
|
function isProtectedPath(pathname: string): boolean {
|
||||||
|
return PROTECTED_ROUTES.some((route: StaticRoute) => pathname === route);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatAuthSync() {
|
||||||
|
const authState = useAuthState();
|
||||||
|
const chatDispatch = useChatDispatch();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
const prevLoginStatusRef = useRef<LoginStatus | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authState.hasInitialized || authState.isLoading) return;
|
||||||
|
|
||||||
|
const prev = prevLoginStatusRef.current;
|
||||||
|
if (prev === authState.loginStatus) return;
|
||||||
|
prevLoginStatusRef.current = authState.loginStatus;
|
||||||
|
|
||||||
|
if (authState.loginStatus === "notLoggedIn") {
|
||||||
|
chatDispatch({ type: "ChatLogout" });
|
||||||
|
if (isProtectedPath(pathname)) {
|
||||||
|
router.replace(ROUTES.splash);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authState.loginStatus === "guest") {
|
||||||
|
chatDispatch({ type: "ChatGuestLogin" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
void (async () => {
|
||||||
|
const tokenR = await AuthStorage.getInstance().getLoginToken();
|
||||||
|
if (!cancelled && tokenR.success && tokenR.data) {
|
||||||
|
chatDispatch({ type: "ChatNonGuestLogin", token: tokenR.data });
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
authState.hasInitialized,
|
||||||
|
authState.isLoading,
|
||||||
|
authState.loginStatus,
|
||||||
|
chatDispatch,
|
||||||
|
pathname,
|
||||||
|
router,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* 事件名与原 Dart ChatEvent 对齐。
|
* 事件名与原 Dart ChatEvent 对齐。
|
||||||
*
|
*
|
||||||
* 历史:原本有 `ChatAuthStatusChanged`(chat 机器刷新 isGuest)—— 已删。
|
* 历史:原本有 `ChatAuthStatusChanged`(chat 机器刷新 isGuest)—— 已删。
|
||||||
* 鉴权状态变化(loginStatus)由 chat-screen 通过 `useAuthState()` 订阅,
|
* 鉴权状态变化(loginStatus)由 <ChatAuthSync /> 通过 `useAuthState()` 订阅,
|
||||||
* chat 机器不感知鉴权。
|
* chat 机器不感知鉴权。
|
||||||
*
|
*
|
||||||
* 鉴权生命周期事件(本轮新增):
|
* 鉴权生命周期事件(本轮新增):
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* chat-screen 不直接创建 ChatWebSocket / 不读 AuthStorage(除 token 取值)。
|
* chat-screen 不直接创建 ChatWebSocket / 不读 AuthStorage(除 token 取值)。
|
||||||
*/
|
*/
|
||||||
export type ChatEvent =
|
export type ChatEvent =
|
||||||
// 鉴权生命周期(用户显式触发登录后由 chat-screen 派)
|
// 鉴权生命周期(登录态变化后由 ChatAuthSync 派)
|
||||||
| { type: "ChatGuestLogin" }
|
| { type: "ChatGuestLogin" }
|
||||||
| { type: "ChatNonGuestLogin"; token: string }
|
| { type: "ChatNonGuestLogin"; token: string }
|
||||||
| { type: "ChatLogout" }
|
| { type: "ChatLogout" }
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
* Chat 状态机(XState v5)
|
* Chat 状态机(XState v5)
|
||||||
*
|
*
|
||||||
* 鉴权解耦(事件驱动):
|
* 鉴权解耦(事件驱动):
|
||||||
* chat 机器不感知鉴权 / 不管 WebSocket —— 由 chat-screen 派生 loginStatus
|
* chat 机器不感知鉴权 / 不管 WebSocket —— 由 <ChatAuthSync /> 派生 loginStatus
|
||||||
* chat-screen 只派 3 个事件:
|
* ChatAuthSync 只派 3 个事件:
|
||||||
* - `ChatGuestLogin` → 游客会话(断 WS = 不连)
|
* - `ChatGuestLogin` → 游客会话(断 WS = 不连)
|
||||||
* - `ChatNonGuestLogin { token }` → 非游客会话(连 WS)
|
* - `ChatNonGuestLogin { token }` → 非游客会话(连 WS)
|
||||||
* - `ChatLogout` → 登出(机器自动 cleanup WS actor)
|
* - `ChatLogout` → 登出(机器自动 cleanup WS actor)
|
||||||
@@ -221,6 +221,10 @@ export const chatMachine = setup({
|
|||||||
// guestSession(游客会话 —— 不连 WS,2 个 init 任务并行)
|
// guestSession(游客会话 —— 不连 WS,2 个 init 任务并行)
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
guestSession: {
|
guestSession: {
|
||||||
|
on: {
|
||||||
|
ChatLogout: "#chat.idle",
|
||||||
|
ChatNonGuestLogin: "#chat.userSession",
|
||||||
|
},
|
||||||
initial: "initializing",
|
initial: "initializing",
|
||||||
states: {
|
states: {
|
||||||
initializing: {
|
initializing: {
|
||||||
@@ -311,8 +315,6 @@ export const chatMachine = setup({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
// 删除 ChatLoadMoreHistory handler —— 游客无服务端 history,不支持翻页
|
// 删除 ChatLoadMoreHistory handler —— 游客无服务端 history,不支持翻页
|
||||||
ChatLogout: "#chat.idle",
|
|
||||||
ChatNonGuestLogin: "#chat.userSession",
|
|
||||||
ChatAISentenceReceived: { actions: "appendOrUpdateAISentence" },
|
ChatAISentenceReceived: { actions: "appendOrUpdateAISentence" },
|
||||||
ChatWebSocketError: { actions: "appendSocketErrorMessage" },
|
ChatWebSocketError: { actions: "appendSocketErrorMessage" },
|
||||||
ChatWebSocketConnected: { actions: "setWsConnected" },
|
ChatWebSocketConnected: { actions: "setWsConnected" },
|
||||||
@@ -355,6 +357,12 @@ export const chatMachine = setup({
|
|||||||
// - sendingViaWs 重新声明 `ChatWebSocketError` 加 target: "ready"(带 action)
|
// - sendingViaWs 重新声明 `ChatWebSocketError` 加 target: "ready"(带 action)
|
||||||
// —— XState v5: child handler 替换 parent;复制 action
|
// —— XState v5: child handler 替换 parent;复制 action
|
||||||
on: {
|
on: {
|
||||||
|
ChatLogout: "#chat.idle",
|
||||||
|
ChatGuestLogin: "#chat.guestSession",
|
||||||
|
ChatNonGuestLogin: {
|
||||||
|
target: "#chat.userSession",
|
||||||
|
reenter: true,
|
||||||
|
},
|
||||||
ChatAISentenceReceived: { actions: "appendOrUpdateAISentence" },
|
ChatAISentenceReceived: { actions: "appendOrUpdateAISentence" },
|
||||||
ChatWebSocketError: { actions: "appendSocketErrorMessage" },
|
ChatWebSocketError: { actions: "appendSocketErrorMessage" },
|
||||||
ChatWebSocketConnected: { actions: "setWsConnected" },
|
ChatWebSocketConnected: { actions: "setWsConnected" },
|
||||||
@@ -424,8 +432,6 @@ export const chatMachine = setup({
|
|||||||
ChatLoadMoreHistory: {
|
ChatLoadMoreHistory: {
|
||||||
target: "loadingMore",
|
target: "loadingMore",
|
||||||
},
|
},
|
||||||
ChatLogout: "#chat.idle",
|
|
||||||
ChatGuestLogin: "#chat.guestSession",
|
|
||||||
ChatQuotaExceeded: { actions: "incrementQuotaExceeded" },
|
ChatQuotaExceeded: { actions: "incrementQuotaExceeded" },
|
||||||
// 注:ChatAISentenceReceived / ChatWebSocketError / ChatWebSocketConnected
|
// 注:ChatAISentenceReceived / ChatWebSocketError / ChatWebSocketConnected
|
||||||
// 已上提到 userSession.on,子级不再声明
|
// 已上提到 userSession.on,子级不再声明
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* @file Automatically generated by barrelsby.
|
* @file Automatically generated by barrelsby.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * from "./chat-auth-sync";
|
||||||
export * from "./chat-context";
|
export * from "./chat-context";
|
||||||
export * from "./chat-events";
|
export * from "./chat-events";
|
||||||
export * from "./chat-machine.actors";
|
export * from "./chat-machine.actors";
|
||||||
|
|||||||
@@ -1,35 +1,39 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auth → User 启动同步器
|
* Auth → User 同步器
|
||||||
*
|
*
|
||||||
* AuthStatusChecker 只负责恢复 loginStatus;头像等登录用户资料由
|
* AuthStatusChecker 只负责恢复 loginStatus;用户资料由 user machine
|
||||||
* UserStorage 持久化在 user machine 这一侧。冷启动直达 /chat 时,
|
* 管理。这里持续监听登录态变化:
|
||||||
* 没有经过 splash/auth 的跳转副作用,因此需要在登录态恢复后主动
|
* - notLoggedIn → 清空本地 user context
|
||||||
* hydrate user store。
|
* - guest / email / OAuth → 重新 hydrate user store
|
||||||
*/
|
*/
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import type { LoginStatus } from "@/data/dto/auth";
|
||||||
import { useAuthState } from "@/stores/auth/auth-context";
|
import { useAuthState } from "@/stores/auth/auth-context";
|
||||||
|
|
||||||
import { useUserDispatch } from "./user-context";
|
import { useUserDispatch } from "./user-context";
|
||||||
|
|
||||||
export function UserAuthSync() {
|
export function UserAuthSync() {
|
||||||
const { loginStatus } = useAuthState();
|
const { hasInitialized, isLoading, loginStatus } = useAuthState();
|
||||||
const userDispatch = useUserDispatch();
|
const userDispatch = useUserDispatch();
|
||||||
const lastInitializedStatusRef = useRef<string | null>(null);
|
const prevLoginStatusRef = useRef<LoginStatus | null>(null);
|
||||||
|
|
||||||
//TODO 在测位栏界面监听变化,监听登录状态的变化若发生变化,则需要分发 user init 事件
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loginStatus === "notLoggedIn" || loginStatus === "guest") {
|
if (!hasInitialized || isLoading) return;
|
||||||
lastInitializedStatusRef.current = null;
|
|
||||||
|
const prev = prevLoginStatusRef.current;
|
||||||
|
if (prev === loginStatus) return;
|
||||||
|
prevLoginStatusRef.current = loginStatus;
|
||||||
|
|
||||||
|
if (loginStatus === "notLoggedIn") {
|
||||||
|
userDispatch({ type: "UserClearLocal" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastInitializedStatusRef.current === loginStatus) return;
|
|
||||||
lastInitializedStatusRef.current = loginStatus;
|
|
||||||
userDispatch({ type: "UserInit" });
|
userDispatch({ type: "UserInit" });
|
||||||
}, [loginStatus, userDispatch]);
|
}, [hasInitialized, isLoading, loginStatus, userDispatch]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user