From e85963e7bf6d6afb9568781d3df77a5261db29bb Mon Sep 17 00:00:00 2001 From: chenhang Date: Wed, 17 Jun 2026 09:52:45 +0800 Subject: [PATCH] =?UTF-8?q?refactor(auth):=20rename=20AuthStatusCheckSubmi?= =?UTF-8?q?tted=20=E2=86=92=20AuthInit=20(init-only)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous event name implied "re-check status", but the actual semantics is just "read storage on app start and sync loginStatus into the machine". It was being dispatched from three sites: 1. AuthStatusChecker mount useEffect (the legit one-time init) 2. AuthStatusChecker loginStatus-watching useEffect (dead code — the machine's onDone already writes loginStatus, re-reading storage just returns the same value as a no-op) 3. Sidebar post-logout effect (also dead code — AuthReset directly sets loginStatus to initialState's "notLoggedIn", and userLogoutActor already cleared storage, so the values are already aligned) This commit: - Renames event AuthStatusCheckSubmitted → AuthInit in: * auth-events.ts (type union) * auth-machine.ts (handler + transition target: checkingAuthStatus → initializing, mirroring UserInit / initializing in user-machine) * auth-status-checker.tsx (single dispatch site) * root-providers.tsx (one comment) - Simplifies auth-status-checker.tsx from 74 → ~40 lines: * Removes useEffect ② (loginStatus-watching re-check) * Removes prevLoginStatusRef + skipNextChangeRef + useAuthState (dead loop-guard machinery no longer needed) - Removes the post-logout re-check dispatch from sidebar-screen.tsx: * AuthReset alone is sufficient — userLogoutActor cleared storage and AuthReset writes back initialState, so they match by construction. No re-verification needed. Net: -44 lines, no behavior change for the happy paths (startup, login, logout), one fewer source of false re-checks. --- src/app/sidebar/components/sidebar-screen.tsx | 8 +-- src/providers/root-providers.tsx | 2 +- src/stores/auth/auth-events.ts | 4 +- src/stores/auth/auth-machine.ts | 11 +-- src/stores/auth/auth-status-checker.tsx | 69 ++++--------------- 5 files changed, 25 insertions(+), 69 deletions(-) diff --git a/src/app/sidebar/components/sidebar-screen.tsx b/src/app/sidebar/components/sidebar-screen.tsx index 6af18836..a7870cae 100644 --- a/src/app/sidebar/components/sidebar-screen.tsx +++ b/src/app/sidebar/components/sidebar-screen.tsx @@ -51,12 +51,10 @@ export function SidebarScreen() { const isNowLoggedOut = user.currentUser == null; prevUserRef.current = user.currentUser; if (wasLoggedIn && isNowLoggedOut) { - // 1) AuthReset:清场(清 email/password/username 等敏感字段) + // AuthReset:清场(context 回到 initialState,loginStatus="notLoggedIn") + // 不需要再 dispatch AuthInit 校验 storage —— userLogoutActor 已清空 storage, + // AuthReset 直接写回 initialState 已与 storage 对齐。 authDispatch({ type: "AuthReset" }); - // 2) AuthStatusCheckSubmitted:通过 storage 重新验证实际 auth 状态 - // (userLogoutActor 已清空 userStorage,checkAuthStatusActor 会返 "notLoggedIn") - // 这一步确保 redirect 到 /chat 后,chat-screen 看到的是真实 storage 状态而非 AuthReset 直接置的 initialState - authDispatch({ type: "AuthStatusCheckSubmitted" }); router.replace(ROUTES.chat); } }, [user.currentUser, authDispatch, router]); diff --git a/src/providers/root-providers.tsx b/src/providers/root-providers.tsx index a4971008..4f05aefc 100644 --- a/src/providers/root-providers.tsx +++ b/src/providers/root-providers.tsx @@ -5,7 +5,7 @@ * * 把所有功能 Provider 串起来: * AuthProvider → UserProvider → SidebarProvider → ChatProvider - * + AuthStatusChecker(启动时一次:派发 AuthStatusCheckSubmitted) + * + AuthStatusChecker(启动时一次:派发 AuthInit) * + OAuthSessionSync (持续监听 NextAuth session → auth machine) * * 它们始终挂载,保证各页面直接 `use*State()` / `use*Dispatch()` 即可, diff --git a/src/stores/auth/auth-events.ts b/src/stores/auth/auth-events.ts index 7ac1fae9..985d6b24 100644 --- a/src/stores/auth/auth-events.ts +++ b/src/stores/auth/auth-events.ts @@ -13,8 +13,8 @@ export type AuthEvent = | { type: "AuthModeChanged"; mode: AuthMode } | { type: "AuthFormCleared" } | { type: "AuthReset" } - /** 启动 / 恢复时检查登录态 —— 拿 deviceId → 查 token(不自动创建游客账号) */ - | { type: "AuthStatusCheckSubmitted" } + /** App 启动时一次性派发 —— 读 storage 把 loginStatus 同步到状态机 */ + | { type: "AuthInit" } /** 显式游客登录 —— splash 上点"游客模式"按钮触发(不自动) */ | { type: "AuthGuestLoginSubmitted" } // 业务事件(提交) diff --git a/src/stores/auth/auth-machine.ts b/src/stores/auth/auth-machine.ts index cc0c8e4e..c00fe72c 100644 --- a/src/stores/auth/auth-machine.ts +++ b/src/stores/auth/auth-machine.ts @@ -31,7 +31,7 @@ export type { AuthEvent } from "./auth-events"; // 在登录成功的 onDone 里自动置 true: // splash / auth screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` 跳 /chat // 跳完派 `AuthClearPendingRedirect` 置 false -// checkingAuthStatus 不设 flag —— 被动查 token 不算"用户意图"。 +// `initializing` 不设 flag —— 启动 init 是被动读 token,不算"用户意图"。 // ============================================================ export const authMachine = setup({ types: { @@ -48,6 +48,7 @@ export const authMachine = setup({ checkAuthStatus: checkAuthStatusActor, }, }).createMachine({ + /** @xstate-layout N4IgpgJg5mDOIC5QEMCuAXAFgOgJYQBswBiAQQ0wAVkA7MAgWQHsIwBhTWmCAbQAYAuolAAHJrFzpcTGsJAAPRABYAHNgDMAVgCMmzepUB2AGwAmLUu1KANCACeiXYe0bthlQE4PxlUp2GlAF9A2zQsPEIScixmVg4uSH4hJBAxCSkZOUUEH2w+dQ8+E2Ntb3VjTVNbBwQrQ01scw9DQs0ldXVdYNCKCKIyCgAxJgAnAFs2ImQRxME5NMlpWRTsw3Vsbz4PUxN6v0rDasd1FuxtEp8rPlNtbVNTbpAwnHx+6MwAJTgwdCT58UWmRWiC8GxuRm82i27iKR1qphU6z4iM0hnuxi20Mezz6UQoAGV0Mh0KhYBwwABjADW+NQACMxpJ0LNkqIARllqBskpChoEXdTB5bkolHwlMY4R4VKZsG0fFpNMY-IY0djeq88VgAOKoODoAAyTCguBotIZTJZ-3SSyyyl56n59yFVlF4rhmi2jXU7TaKuM2xumjV4Q1AywAFExshcARDcbTfTGehmbw5ikFhzbQgeXw+SoBc6RWKJfZEAEGp4+JoPP4ijzDMGXpEw5hI9GCF9jbBmSMzUmU390+ybcDs-bHYLha6SzVzttZbo9MYAhjqw2Qk91c33lqmEaiHGTX2LanWalh0CuXbcw7806p8W4W4lLL3HdysirPcVI3cS3BsgFJgHSe5UoeCbmsmlpDtal4KNeeYFg+bqlgg9wNEUd5WNW9YPBuOKhu8pAiCIB5GkeiYnoObKwZy8HZjKlTepUIraFonRPtoRh5FW4qdKUhhFHhPQhtuFC7vuYD4nYNAUseUGnlagJ0dy453pOLqPqh2EVtWnTGFo9wOuov6EUMgHAaB0myfJA5pjRylZjmiH3ppKGzjcHiNPkRTVpovhiuuIlNm8FCTGA0yUGANAQCaUBfLFMwUr89nnrRTlVNpmJgms+jNCoHpKEFm7hBSmCUlScXvISxKksQEAyGAeA0AAbkwVJNTiZUVVVBJEiSsAICabUUsSSxJNRaWOaOeiGNgLQ+Gs+TGP6qJwionjYOKRhfh4HSih4v7ddSvVYDVA3EGAIwjKM2CkcSABmoxjNgXXlSdNBQNV-WkkNrVMKNHITalGYjleCB6GoSp8CU3imDo5iHKhC0aK0e3XDcxa-gQTDILFn06nq4H1Y1zVtR1r29DjeNxYT3bgX9I1jTIwNnqDcHZHpYJcQZ7QOtcSM1JYxhnIq5haFWPj+djuP41AdMGuRNCXddt33egT3jJT4TU3LCsM8NAPMzQrNKZmM0eC+JiKlC1znOosKocZeRvgK3oipbxgyzTn1tjGxMNXQZPtZ1VOy3FfuxkrjNG0DgiTezKmIH5Gww+YYpbP6yJwhUMp3kVBheDo3re3LkfE1dN0jHdBCPc92s4LrEdRv70eG4D43xyDF5JxD3jzTyzT+lKtwbe61bzd6hlmNKKraKXzftp2uDdldJNB4bFM4k3vstx2YBdj2McdyzXdsz3WY6CLfC6GYbGWyqNjadWXntAEFgBNKJf4WHPtQJHy9V4jBVlXGudctbb3DrvJeB8V5H3bsbU2MFprgyvtgAqe1lxSn5plWcHoRZaD2l+fI-liqQL-gAeXeOvJqm9Q46ygVAKhFBj6ILPmbMG9F-JqClO0REBQ0TnBnIgHYDRUT5lKJ0ZElR54-wYZQ6hlc1a1w1vXchctmFYFYXHAQCcL4zU2tKO8XgCpKncogfMso+DWOuB0LOOFfywBkhSWme4oBEAAEKAQ6jFGhwct69CcbJVxkkvHUmihAbRnddHd3SjNE4Gg9JGFYgYRUT5ygyldjWZEWxvCOOcSE9xYAwk+IgCA5R4CXo4iCS4gmbjPHeIiVE0+MTz5xNQQYbynRvTXBHicJ8ph-TzR8msFQ0JqxezkTgGpcUAJARAu1EpES-F0IbtgGZn05mWUWY0mKzSTbsOQebVBNwNgnHwYqEo9R1CcUGWcUwPI5Q7R8EEKZ6yCmbIsgsqkSzfFKOrurTWVTAkfKgFs75vzIkIJ0Xo9pXD7jzTvPoXQ4p8g3Kyh0PI219q6D2nhDcNAWBwDkM8DhHNEAAFphEIApQ0Lw9KGUMqVEGN5GoyW9zuC+c4oo2gGDFOcfMT5lwygKBiZE0pUTCRKjgY6lVPrfVqvAI5nDVhjNcA8wohd7ginWiqRoqITC6HKN6KV6jaa6npkrdlWYYTqpzFqh5T8hb5A2H4BEdYVA+C9QvaBrd4zWtHLazoGr8ieG1U6xA0NsAYkXJUfyWI3k73-nvQBPYA3gyDZyzVYbHVPlIY0QZV9+GijIb-DR7x030UzSGh1OrtImC2vyN1BwOimpBcEupoTdkQEraqm8WbQ0BjrbOCWr4BZKgKKiPJbyNlgq+aBSFvayxquDfanNw7HC3HWEKUUH5lSVGCMEIAA */ id: "auth", initial: "idle", context: initialState, @@ -78,8 +79,8 @@ export const authMachine = setup({ AuthReset: { actions: assign(() => initialState), }, - // 启动 / 恢复时检查登录态 —— 由 派发 - AuthStatusCheckSubmitted: "checkingAuthStatus", + // 启动一次性 init:从 storage 同步 loginStatus 到状态机 —— 由 派发 + AuthInit: "initializing", AuthGuestLoginSubmitted: "loadingGuestLogin", AuthEmailLoginSubmitted: "loadingEmailLogin", @@ -102,12 +103,12 @@ export const authMachine = setup({ }, }, - checkingAuthStatus: { + initializing: { entry: assign({ errorMessage: null }), invoke: { src: "checkAuthStatus", onDone: { - target: "idle", // ← status check 完回 idle(不再假冒 success) + target: "idle", // ← init 完回 idle(从 storage 拿到 loginStatus 后落地) actions: assign({ loginStatus: ({ event }) => event.output, // 不置 pendingRedirect —— 状态查询是被动读 token,不算"用户意图" diff --git a/src/stores/auth/auth-status-checker.tsx b/src/stores/auth/auth-status-checker.tsx index 03433049..21023c1c 100644 --- a/src/stores/auth/auth-status-checker.tsx +++ b/src/stores/auth/auth-status-checker.tsx @@ -1,74 +1,31 @@ "use client"; /** - * AuthStatusChecker 启动 + 状态变化检查器 + * AuthInit 派发器 —— App 启动时读 storage 同步 loginStatus 到状态机 * - * 触发时机: - * 1. App 启动时(mount)派发一次 `AuthStatusCheckSubmitted` - * 2. loginStatus 每次变化时(外部触发)也派发一次 —— 重新读 storage 校准 - * 例:OAuth sync 完成 → loginStatus 变 "facebook" → re-check 一次确认 storage 一致 - * 邮箱登录成功 → loginStatus 变 "email" → re-check 确认 token 已落盘 + * 派发时机:仅 mount 时一次(`useEffect` deps = `[dispatch]`,永远不重跑)。 * - * 死循环防护(关键 —— 不加会无限递归): - * - 状态机收 `AuthStatusCheckSubmitted` → 进 `checkingAuthStatus` → 读 storage → onDone 写回 loginStatus - * - 如果 useEffect 直接依赖 `loginStatus`,写回本身触发 effect → 再 dispatch → 死循环 - * - 用 `skipNextChangeRef` 抑制一次: - * 每次我们自己 dispatch 前把 flag 置 true; - * effect 见到 loginStatus 变化且 flag 为 true 时跳过并清 flag。 + * 与原 `AuthStatusCheckSubmitted` 的区别: + * - 原版本有两个 useEffect:① mount + ② loginStatus 变化 re-check + * - re-check 是 dead code —— 状态机 onDone 已经把 loginStatus 写对, + * 再读 storage 只会拿到相同值(no-op),徒增死循环防护复杂度 + * - 新版本只保留 ①,命名 `AuthInit` 与 `UserInit` 对齐(user-machine 的同模式) * * 与 平级: - * - AuthStatusChecker: 启动 + 状态变化时 re-check("自己也能感知") - * - OAuthSessionSync: 持续监听 NextAuth session("别人也在写") + * - AuthStatusChecker: 启动时一次性 init("自己启动时同步") + * - OAuthSessionSync: 持续监听 NextAuth session("别人在写") */ -import { useEffect, useRef } from "react"; +import { useEffect } from "react"; -import type { LoginStatus } from "@/models/auth/login-status"; - -import { useAuthDispatch, useAuthState } from "./auth-context"; +import { useAuthDispatch } from "./auth-context"; export function AuthStatusChecker() { const dispatch = useAuthDispatch(); - const loginStatus = useAuthState().loginStatus; - // 上一次的 loginStatus(首次 mount 留 null —— 用来跳过首次 effect 跑) - const prevLoginStatusRef = useRef(null); - // 抑制下一次 loginStatus 变化触发的 effect(自己 dispatch 引起的写回要跳过) - const skipNextChangeRef = useRef(false); - - // ───────────────────────────────────────────────────────────── - // ① App 启动时:派发一次 - // ───────────────────────────────────────────────────────────── useEffect(() => { - console.log("[auth-status-checker] mount → dispatch AuthStatusCheckSubmitted"); - skipNextChangeRef.current = true; // 抑制启动 check 写回 loginStatus 引起的 effect - dispatch({ type: "AuthStatusCheckSubmitted" }); + console.log("[auth-init] mount → dispatch AuthInit"); + dispatch({ type: "AuthInit" }); }, [dispatch]); - // ───────────────────────────────────────────────────────────── - // ② loginStatus 变化时:派发一次(但跳过我们自己 check 写回的那次) - // ───────────────────────────────────────────────────────────── - useEffect(() => { - const prev = prevLoginStatusRef.current; - prevLoginStatusRef.current = loginStatus; - if (prev === null) return; // 首次 mount —— 启动那次由 ① 处理 - if (prev === loginStatus) return; // loginStatus 没变 —— 不触发 - if (skipNextChangeRef.current) { - // 这是我们自己 check 写回的 —— 抑制本次 - skipNextChangeRef.current = false; - console.log( - "[auth-status-checker] loginStatus change suppressed (self-triggered)", - { from: prev, to: loginStatus }, - ); - return; - } - // 外部触发(OAuth sync / 邮箱登录 / 登出 等)的 loginStatus 变化 → 再 check 一次校准 - skipNextChangeRef.current = true; - console.log( - "[auth-status-checker] loginStatus changed externally → re-check", - { from: prev, to: loginStatus }, - ); - dispatch({ type: "AuthStatusCheckSubmitted" }); - }, [loginStatus, dispatch]); - // 无 UI return null; } \ No newline at end of file