import { Post } from '../types'; import { postService } from '../services/postService'; import { CacheBus, CacheEvent } from './cacheBus'; interface PostListPayload { key: string; posts: Post[]; } interface PostDetailPayload { postId: string; post: Post | null; } interface PostManagerSnapshot { listKeys: string[]; detailKeys: string[]; } type PostManagerEvent = | CacheEvent | CacheEvent | CacheEvent | CacheEvent<{ error: unknown; context: string }>; const LIST_TTL = 30 * 1000; const DETAIL_TTL = 60 * 1000; interface CacheEntry { data: T; timestamp: number; ttl: number; } class PostManager extends CacheBus { private listCache = new Map>(); private detailCache = new Map>(); private pendingRequests = new Map>(); protected getSnapshot(): PostManagerSnapshot { return { listKeys: [...this.listCache.keys()], detailKeys: [...this.detailCache.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; } private listKey(type: string, page: number, pageSize: number): string { return `${type}:${page}:${pageSize}`; } async getPosts( type = 'hot', page = 1, pageSize = 20, forceRefresh = false ): Promise { const key = this.listKey(type, page, pageSize); const cached = this.listCache.get(key); if (!forceRefresh && cached && !this.isExpired(cached)) { return cached.data; } if (!forceRefresh && cached && this.isExpired(cached)) { this.refreshPostsInBackground(key, type, page, pageSize); return cached.data; } return this.dedupe(`posts:list:${key}`, async () => { const response = await postService.getPosts(page, pageSize, type); const posts = response.list || []; this.listCache.set(key, { data: posts, timestamp: Date.now(), ttl: LIST_TTL }); this.notify({ type: 'list_updated', payload: { key, posts }, timestamp: Date.now(), }); return posts; }); } private refreshPostsInBackground(key: string, type: string, page: number, pageSize: number): void { this.dedupe(`posts:list:bg:${key}`, async () => { const response = await postService.getPosts(page, pageSize, type); const posts = response.list || []; this.listCache.set(key, { data: posts, timestamp: Date.now(), ttl: LIST_TTL }); this.notify({ type: 'list_updated', payload: { key, posts }, timestamp: Date.now(), }); return posts; }).catch((error) => { this.notify({ type: 'error', payload: { error, context: 'refreshPostsInBackground' }, timestamp: Date.now(), }); }); } async getPostDetail(postId: string, forceRefresh = false): Promise { const cached = this.detailCache.get(postId); if (!forceRefresh && cached && !this.isExpired(cached)) { return cached.data; } if (!forceRefresh && cached && this.isExpired(cached)) { this.refreshPostDetailInBackground(postId); return cached.data; } return this.dedupe(`posts:detail:${postId}`, async () => { const post = await postService.getPost(postId); this.detailCache.set(postId, { data: post, timestamp: Date.now(), ttl: DETAIL_TTL }); this.notify({ type: 'detail_updated', payload: { postId, post }, timestamp: Date.now(), }); return post; }); } private refreshPostDetailInBackground(postId: string): void { this.dedupe(`posts:detail:bg:${postId}`, async () => { const post = await postService.getPost(postId); this.detailCache.set(postId, { data: post, timestamp: Date.now(), ttl: DETAIL_TTL }); this.notify({ type: 'detail_updated', payload: { postId, post }, timestamp: Date.now(), }); return post; }).catch((error) => { this.notify({ type: 'error', payload: { error, context: 'refreshPostDetailInBackground' }, timestamp: Date.now(), }); }); } invalidate(postId?: string): void { if (postId) { this.detailCache.delete(postId); return; } this.listCache.clear(); this.detailCache.clear(); } clear(): void { this.listCache.clear(); this.detailCache.clear(); this.pendingRequests.clear(); } } export const postManager = new PostManager();