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:
174
src/stores/postManager.ts
Normal file
174
src/stores/postManager.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
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(
|
||||
type = 'recommend',
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user