import { UserDTO } from '../types/dto'; import { authService } from '../services/authService'; import { getCurrentUserCache, getUserCache, saveCurrentUserCache, saveUserCache, } from '../services/database'; import { CacheBus, CacheEvent } from './cacheBus'; interface UserManagerSnapshot { hasCurrentUser: boolean; cachedUserIds: string[]; } type UserManagerEvent = | CacheEvent<{ user: UserDTO | null }> | CacheEvent<{ userId: string; user: UserDTO | null }> | CacheEvent | CacheEvent<{ error: unknown; context: string }>; const USER_TTL = 5 * 60 * 1000; interface CacheEntry { data: T; timestamp: number; ttl: number; } class UserManager extends CacheBus { private currentUserCache: CacheEntry | null = null; private userCache = new Map>(); private pendingRequests = new Map>(); protected getSnapshot(): UserManagerSnapshot { return { hasCurrentUser: !!this.currentUserCache?.data, cachedUserIds: [...this.userCache.keys()], }; } private isExpired(entry: CacheEntry): boolean { return Date.now() - entry.timestamp > entry.ttl; } private async dedupe(key: string, fetcher: () => Promise): Promise { const pending = this.pendingRequests.get(key) as Promise | undefined; if (pending) return pending; const request = fetcher().finally(() => { this.pendingRequests.delete(key); }); this.pendingRequests.set(key, request); return request; } async getCurrentUser(forceRefresh = false): Promise { if (!forceRefresh && this.currentUserCache && !this.isExpired(this.currentUserCache)) { return this.currentUserCache.data; } if (!forceRefresh && this.currentUserCache && this.isExpired(this.currentUserCache)) { this.refreshCurrentUserInBackground(); return this.currentUserCache.data; } if (!forceRefresh) { const cachedFromDb = await getCurrentUserCache(); if (cachedFromDb) { this.currentUserCache = { data: cachedFromDb, timestamp: Date.now(), ttl: USER_TTL }; this.notify({ type: 'detail_updated', payload: { user: cachedFromDb }, timestamp: Date.now(), }); this.refreshCurrentUserInBackground(); return cachedFromDb; } } return this.dedupe('users:current', async () => { const user = await authService.fetchCurrentUserFromAPI(); this.currentUserCache = { data: user, timestamp: Date.now(), ttl: USER_TTL }; if (user) { saveCurrentUserCache(user).catch(() => {}); saveUserCache(user).catch(() => {}); } this.notify({ type: 'detail_updated', payload: { user }, timestamp: Date.now(), }); return user; }); } private refreshCurrentUserInBackground(): void { this.dedupe('users:current:bg', async () => { const user = await authService.fetchCurrentUserFromAPI(); this.currentUserCache = { data: user, timestamp: Date.now(), ttl: USER_TTL }; if (user) { saveCurrentUserCache(user).catch(() => {}); saveUserCache(user).catch(() => {}); } this.notify({ type: 'detail_updated', payload: { user }, timestamp: Date.now(), }); return user; }).catch((error) => { this.notify({ type: 'error', payload: { error, context: 'refreshCurrentUserInBackground' }, timestamp: Date.now(), }); }); } async getUserById(userId: string, forceRefresh = false): Promise { const cached = this.userCache.get(userId); if (!forceRefresh && cached && !this.isExpired(cached)) { return cached.data; } if (!forceRefresh && cached && this.isExpired(cached)) { this.refreshUserByIdInBackground(userId); return cached.data; } if (!forceRefresh) { const cachedFromDb = await getUserCache(userId); if (cachedFromDb) { this.userCache.set(userId, { data: cachedFromDb, timestamp: Date.now(), ttl: USER_TTL }); this.notify({ type: 'detail_updated', payload: { userId, user: cachedFromDb }, timestamp: Date.now(), }); this.refreshUserByIdInBackground(userId); return cachedFromDb; } } return this.dedupe(`users:detail:${userId}`, async () => { const user = await authService.fetchUserByIdFromAPI(userId); this.userCache.set(userId, { data: user, timestamp: Date.now(), ttl: USER_TTL }); if (user) { saveUserCache(user).catch(() => {}); } this.notify({ type: 'detail_updated', payload: { userId, user }, timestamp: Date.now(), }); return user; }); } private refreshUserByIdInBackground(userId: string): void { this.dedupe(`users:detail:bg:${userId}`, async () => { const user = await authService.fetchUserByIdFromAPI(userId); this.userCache.set(userId, { data: user, timestamp: Date.now(), ttl: USER_TTL }); if (user) { saveUserCache(user).catch(() => {}); } this.notify({ type: 'detail_updated', payload: { userId, user }, timestamp: Date.now(), }); return user; }).catch((error) => { this.notify({ type: 'error', payload: { error, context: 'refreshUserByIdInBackground' }, timestamp: Date.now(), }); }); } invalidateUsers(): void { this.currentUserCache = null; this.userCache.clear(); } } export const userManager = new UserManager();