diff --git a/src/stores/auth/auth-status-checker.tsx b/src/stores/auth/auth-status-checker.tsx index 1a250b75..03433049 100644 --- a/src/stores/auth/auth-status-checker.tsx +++ b/src/stores/auth/auth-status-checker.tsx @@ -1,25 +1,74 @@ "use client"; /** - * AuthStatusChecker 启动检查器 + * AuthStatusChecker 启动 + 状态变化检查器 * - * App 启动时派发一次 `AuthStatusCheckSubmitted`, - * 让 auth machine 走"拿 deviceId → 查 token → 必要时游客登录"流程。 + * 触发时机: + * 1. App 启动时(mount)派发一次 `AuthStatusCheckSubmitted` + * 2. loginStatus 每次变化时(外部触发)也派发一次 —— 重新读 storage 校准 + * 例:OAuth sync 完成 → loginStatus 变 "facebook" → re-check 一次确认 storage 一致 + * 邮箱登录成功 → loginStatus 变 "email" → re-check 确认 token 已落盘 + * + * 死循环防护(关键 —— 不加会无限递归): + * - 状态机收 `AuthStatusCheckSubmitted` → 进 `checkingAuthStatus` → 读 storage → onDone 写回 loginStatus + * - 如果 useEffect 直接依赖 `loginStatus`,写回本身触发 effect → 再 dispatch → 死循环 + * - 用 `skipNextChangeRef` 抑制一次: + * 每次我们自己 dispatch 前把 flag 置 true; + * effect 见到 loginStatus 变化且 flag 为 true 时跳过并清 flag。 * * 与 平级: - * - AuthStatusChecker: 一次性 init - * - OAuthSessionSync: 持续监听 NextAuth session + * - AuthStatusChecker: 启动 + 状态变化时 re-check("自己也能感知") + * - OAuthSessionSync: 持续监听 NextAuth session("别人也在写") */ -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; -import { useAuthDispatch } from "./auth-context"; +import type { LoginStatus } from "@/models/auth/login-status"; + +import { useAuthDispatch, useAuthState } 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" }); }, [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