Files
frontend/src/stores/userManager.ts
lan 3968660048 Initial frontend repository commit.
Include app source and update .gitignore to exclude local release artifacts and signing files.

Made-with: Cursor
2026-03-09 21:29:03 +08:00

189 lines
5.6 KiB
TypeScript

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<UserManagerSnapshot>
| CacheEvent<{ error: unknown; context: string }>;
const USER_TTL = 5 * 60 * 1000;
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
}
class UserManager extends CacheBus<UserManagerSnapshot, UserManagerEvent> {
private currentUserCache: CacheEntry<UserDTO | null> | null = null;
private userCache = new Map<string, CacheEntry<UserDTO | null>>();
private pendingRequests = new Map<string, Promise<any>>();
protected getSnapshot(): UserManagerSnapshot {
return {
hasCurrentUser: !!this.currentUserCache?.data,
cachedUserIds: [...this.userCache.keys()],
};
}
private isExpired(entry: CacheEntry<any>): boolean {
return Date.now() - entry.timestamp > entry.ttl;
}
private async dedupe<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
const pending = this.pendingRequests.get(key) as Promise<T> | 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<UserDTO | null> {
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<UserDTO | null> {
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();