Initial frontend repository commit.
Include app source and update .gitignore to exclude local release artifacts and signing files. Made-with: Cursor
This commit is contained in:
188
src/stores/userManager.ts
Normal file
188
src/stores/userManager.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user