2026-03-09 21:29:03 +08:00
|
|
|
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<PostListPayload>
|
|
|
|
|
| CacheEvent<PostDetailPayload>
|
|
|
|
|
| CacheEvent<PostManagerSnapshot>
|
|
|
|
|
| CacheEvent<{ error: unknown; context: string }>;
|
|
|
|
|
|
|
|
|
|
const LIST_TTL = 30 * 1000;
|
|
|
|
|
const DETAIL_TTL = 60 * 1000;
|
|
|
|
|
|
|
|
|
|
interface CacheEntry<T> {
|
|
|
|
|
data: T;
|
|
|
|
|
timestamp: number;
|
|
|
|
|
ttl: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class PostManager extends CacheBus<PostManagerSnapshot, PostManagerEvent> {
|
|
|
|
|
private listCache = new Map<string, CacheEntry<Post[]>>();
|
|
|
|
|
private detailCache = new Map<string, CacheEntry<Post | null>>();
|
|
|
|
|
private pendingRequests = new Map<string, Promise<any>>();
|
|
|
|
|
|
|
|
|
|
protected getSnapshot(): PostManagerSnapshot {
|
|
|
|
|
return {
|
|
|
|
|
listKeys: [...this.listCache.keys()],
|
|
|
|
|
detailKeys: [...this.detailCache.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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private listKey(type: string, page: number, pageSize: number): string {
|
|
|
|
|
return `${type}:${page}:${pageSize}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getPosts(
|
2026-03-24 05:18:22 +08:00
|
|
|
type = 'hot',
|
2026-03-09 21:29:03 +08:00
|
|
|
page = 1,
|
|
|
|
|
pageSize = 20,
|
|
|
|
|
forceRefresh = false
|
|
|
|
|
): Promise<Post[]> {
|
|
|
|
|
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<Post | null> {
|
|
|
|
|
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();
|
|
|
|
|
|