Prmbr/src/hooks/useBetterAuth.ts
2025-08-30 12:48:06 +08:00

342 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useEffect, useState, useCallback } from 'react'
import { useSession } from '@/lib/auth-client'
import { userCache, UserData, SyncTrigger, SyncOptions, shouldSync } from '@/lib/user-cache'
import type { User } from '@/lib/auth'
// 全局防抖变量
const debounceTimeouts: Map<string, NodeJS.Timeout> = new Map()
const pendingPromises: Map<string, Promise<void>> = new Map()
interface AuthUserState {
user: User | null
userData: UserData | null
loading: boolean
isAdmin: boolean
}
// 全局状态管理,避免多个钩子实例重复调用
let globalAuthState: AuthUserState = {
user: null,
userData: null,
loading: true,
isAdmin: false,
}
const stateListeners: Set<(state: AuthUserState) => void> = new Set()
let isGlobalInitialized = false
let currentUserId: string | null = null
let lastSyncTime: number = 0
// 广播状态变化给所有监听器
const notifyStateChange = (newState: AuthUserState) => {
globalAuthState = { ...newState }
stateListeners.forEach(listener => listener(newState))
}
export function useBetterAuth() {
const [state, setState] = useState<AuthUserState>(globalAuthState)
const { data: session, isPending } = useSession()
// 注册状态监听器
useEffect(() => {
const listener = (newState: AuthUserState) => {
setState(newState)
}
stateListeners.add(listener)
return () => {
stateListeners.delete(listener)
}
}, [])
// 全局同步函数,防止重复调用
const globalSyncUserToDatabase = useCallback(async (userId: string, options: SyncOptions) => {
// 检查是否在短时间内已经同步过
const now = Date.now()
if (!options.force && now - lastSyncTime < 5000) { // 5秒内不重复同步
return
}
if (!shouldSync(userId, options)) {
return
}
return userCache.getSyncPromise(userId, async () => {
try {
const response = await fetch('/api/users/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
if (!response.ok) {
throw new Error(`Sync failed: ${response.status}`)
}
userCache.recordSync(userId)
lastSyncTime = Date.now()
console.log(`User synced to database: ${userId} (trigger: ${options.trigger})`)
} catch (error) {
console.error('Failed to sync user to database:', error)
throw error
}
})
}, [])
// 全局获取用户数据函数
const globalFetchUserData = useCallback(async (userId: string, options: SyncOptions) => {
// 先检查缓存
if (!options.skipCache) {
const cachedData = userCache.getUserData(userId)
if (cachedData) {
const newState = {
...globalAuthState,
userData: cachedData,
isAdmin: cachedData.isAdmin || false,
}
notifyStateChange(newState)
return cachedData
}
}
try {
const response = await fetch('/api/users/sync')
if (!response.ok) {
throw new Error(`Failed to fetch user data: ${response.status}`)
}
const data = await response.json()
const userData = data.user as UserData
// 更新缓存
userCache.setUserData(userId, userData)
// 更新全局状态
const newState = {
...globalAuthState,
userData,
isAdmin: userData.isAdmin || false,
}
notifyStateChange(newState)
return userData
} catch (error) {
console.error('Failed to fetch user data:', error)
throw error
}
}, [])
// 立即更新用户状态(不防抖)
const updateUserState = useCallback((user: User | null) => {
const newState = {
...globalAuthState,
user,
loading: false,
}
notifyStateChange(newState)
// 如果用户变化了,清除旧用户的缓存
if (currentUserId && currentUserId !== user?.id) {
userCache.clearUserCache(currentUserId)
lastSyncTime = 0 // 重置同步时间
}
currentUserId = user?.id || null
if (!user) {
const userLoggedOutState = {
...globalAuthState,
user: null,
userData: null,
isAdmin: false,
loading: false,
}
notifyStateChange(userLoggedOutState)
}
}, [])
// 全局处理用户数据同步(可以防抖)
const globalHandleUserDataSync = useCallback(async (
user: User | null,
trigger: SyncTrigger = SyncTrigger.INITIAL_LOAD
) => {
if (!user) return
// 避免重复处理相同用户
if (trigger === SyncTrigger.INITIAL_LOAD && isGlobalInitialized && user.id === currentUserId) {
return
}
try {
const syncOptions: SyncOptions = { trigger }
// 并行执行同步和获取用户数据,但添加去重机制
await Promise.all([
globalSyncUserToDatabase(user.id, syncOptions),
globalFetchUserData(user.id, syncOptions),
])
console.log(`User data loaded: ${user.id} (trigger: ${trigger})`)
} catch (error) {
console.error('Failed to handle user change:', error)
}
}, [globalSyncUserToDatabase, globalFetchUserData])
// 创建防抖版本的用户数据同步处理
const debouncedUserDataSync = useCallback(async (
user: User | null,
trigger: SyncTrigger = SyncTrigger.INITIAL_LOAD
) => {
const key = `${user?.id || 'anonymous'}-${trigger}`
// 清除之前的超时
const existingTimeout = debounceTimeouts.get(key)
if (existingTimeout) {
clearTimeout(existingTimeout)
}
// 如果有正在执行的Promise直接返回
const existingPromise = pendingPromises.get(key)
if (existingPromise) {
return existingPromise
}
// 创建新的防抖Promise
const debouncePromise = new Promise<void>((resolve) => {
const timeout = setTimeout(async () => {
debounceTimeouts.delete(key)
try {
await globalHandleUserDataSync(user, trigger)
} finally {
pendingPromises.delete(key)
resolve()
}
}, 300)
debounceTimeouts.set(key, timeout)
})
pendingPromises.set(key, debouncePromise)
return debouncePromise
}, [globalHandleUserDataSync])
// 监听 Better Auth 会话变化
useEffect(() => {
let mounted = true
const updateAuthState = async () => {
if (!mounted) return
const user = session?.user || null
const trigger = !isGlobalInitialized ? SyncTrigger.INITIAL_LOAD : SyncTrigger.SIGN_IN
// 立即更新用户状态(不延迟)
updateUserState(user)
// 异步进行数据同步(防抖延迟)
if (user) {
debouncedUserDataSync(user, trigger)
}
if (!isGlobalInitialized) {
isGlobalInitialized = true
}
}
if (!isPending) {
updateAuthState()
}
return () => {
mounted = false
}
}, [session, isPending, updateUserState, debouncedUserDataSync])
// 设置loading状态
useEffect(() => {
const newState = {
...globalAuthState,
loading: isPending
}
notifyStateChange(newState)
}, [isPending])
// 手动刷新用户数据
const refreshUserData = useCallback(async (force = false) => {
if (!globalAuthState.user) return
try {
notifyStateChange({ ...globalAuthState, loading: true })
const syncOptions: SyncOptions = {
trigger: SyncTrigger.MANUAL_REFRESH,
force,
skipCache: force,
}
await Promise.all([
globalSyncUserToDatabase(globalAuthState.user.id, syncOptions),
globalFetchUserData(globalAuthState.user.id, syncOptions),
])
} catch (error) {
console.error('Failed to refresh user data:', error)
} finally {
notifyStateChange({ ...globalAuthState, loading: false })
}
}, [globalSyncUserToDatabase, globalFetchUserData])
// 登出
const signOut = useCallback(async () => {
try {
const { signOut: betterSignOut } = await import('@/lib/auth-client')
await betterSignOut()
// 清除缓存
if (currentUserId) {
userCache.clearUserCache(currentUserId)
}
// 重置全局状态
isGlobalInitialized = false
currentUserId = null
lastSyncTime = 0
// 重定向到首页
window.location.href = '/'
} catch (error) {
console.error('Failed to sign out:', error)
}
}, [])
// 触发用户信息更新(用于个人资料更新后)
const triggerProfileUpdate = useCallback(async () => {
if (!globalAuthState.user) return
try {
await globalHandleUserDataSync(globalAuthState.user, SyncTrigger.PROFILE_UPDATE)
} catch (error) {
console.error('Failed to update profile:', error)
}
}, [globalHandleUserDataSync])
// 触发订阅状态更新
const triggerSubscriptionUpdate = useCallback(async () => {
if (!globalAuthState.user) return
try {
await globalHandleUserDataSync(globalAuthState.user, SyncTrigger.SUBSCRIPTION_CHANGE)
} catch (error) {
console.error('Failed to update subscription:', error)
}
}, [globalHandleUserDataSync])
return {
user: state.user,
userData: state.userData,
loading: state.loading,
isAdmin: state.isAdmin,
signOut,
refreshUserData,
triggerProfileUpdate,
triggerSubscriptionUpdate,
}
}