refactor(stores): unify data sources pattern and extract BaseManager base class
- Add BaseManager/CachedManager base classes to eliminate duplicate cache logic - Add postListSources.ts with IPostListPagedSource interface - Add groupListSources.ts with IGroupListPagedSource interface - Refactor postManager to use CachedManager and Sources pattern - Refactor groupManager to use CachedManager and Sources pattern - Clean up duplicate methods in groupService.ts - Fix TypeScript errors and remove mock data
This commit is contained in:
127
src/stores/baseManager.ts
Normal file
127
src/stores/baseManager.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* BaseManager - 通用 Manager 基类
|
||||
*
|
||||
* 提取自 postManager/groupManager/userManager 的重复逻辑:
|
||||
* - 请求去重 (dedupe)
|
||||
* - 缓存条目管理 (CacheEntry)
|
||||
* - 过期检查 (isExpired)
|
||||
*/
|
||||
|
||||
import { CacheBus, CacheEvent } from './cacheBus';
|
||||
|
||||
/** 缓存条目结构 */
|
||||
export interface CacheEntry<T> {
|
||||
data: T;
|
||||
timestamp: number;
|
||||
ttl: number;
|
||||
}
|
||||
|
||||
/** 创建缓存条目 */
|
||||
export function createCacheEntry<T>(data: T, ttl: number): CacheEntry<T> {
|
||||
return { data, timestamp: Date.now(), ttl };
|
||||
}
|
||||
|
||||
/** 检查缓存是否过期 */
|
||||
export function isCacheExpired(entry: CacheEntry<any>): boolean {
|
||||
return Date.now() - entry.timestamp > entry.ttl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用 Manager 基类
|
||||
*
|
||||
* 子类只需关注业务逻辑,无需重复实现缓存和去重机制
|
||||
*/
|
||||
export abstract class BaseManager<
|
||||
TSnapshot,
|
||||
TEvent extends CacheEvent = CacheEvent
|
||||
> extends CacheBus<TSnapshot, TEvent> {
|
||||
/** 待处理请求 Map,用于去重 */
|
||||
protected pendingRequests = new Map<string, Promise<any>>();
|
||||
|
||||
/**
|
||||
* 请求去重:相同 key 的并发请求会被合并为一个
|
||||
* @param key 唯一标识
|
||||
* @param fetcher 实际请求函数
|
||||
*/
|
||||
protected 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;
|
||||
}
|
||||
|
||||
/** 清除所有待处理请求 */
|
||||
protected clearPendingRequests(): void {
|
||||
this.pendingRequests.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 带缓存的 Manager 基类
|
||||
*
|
||||
* 进一步封装内存缓存逻辑
|
||||
*/
|
||||
export abstract class CachedManager<
|
||||
TSnapshot,
|
||||
TEvent extends CacheEvent = CacheEvent
|
||||
> extends BaseManager<TSnapshot, TEvent> {
|
||||
/** 内存缓存 Map */
|
||||
protected memoryCache = new Map<string, CacheEntry<any>>();
|
||||
|
||||
/**
|
||||
* 获取缓存
|
||||
* @param key 缓存 key
|
||||
* @returns 缓存数据,过期或不存在返回 null
|
||||
*/
|
||||
protected getFromCache<T>(key: string): T | null {
|
||||
const entry = this.memoryCache.get(key) as CacheEntry<T> | undefined;
|
||||
if (!entry) return null;
|
||||
if (isCacheExpired(entry)) {
|
||||
this.memoryCache.delete(key);
|
||||
return null;
|
||||
}
|
||||
return entry.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存
|
||||
* @param key 缓存 key
|
||||
* @param data 数据
|
||||
* @param ttl 过期时间(毫秒)
|
||||
*/
|
||||
protected setCache<T>(key: string, data: T, ttl: number): void {
|
||||
this.memoryCache.set(key, createCacheEntry(data, ttl));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存是否存在且未过期
|
||||
*/
|
||||
protected hasValidCache(key: string): boolean {
|
||||
const entry = this.memoryCache.get(key);
|
||||
if (!entry) return false;
|
||||
return !isCacheExpired(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存是否存在但已过期
|
||||
*/
|
||||
protected hasExpiredCache(key: string): boolean {
|
||||
const entry = this.memoryCache.get(key);
|
||||
if (!entry) return false;
|
||||
return isCacheExpired(entry);
|
||||
}
|
||||
|
||||
/** 清除所有缓存 */
|
||||
protected clearCache(): void {
|
||||
this.memoryCache.clear();
|
||||
}
|
||||
|
||||
/** 清除指定 key 的缓存 */
|
||||
protected deleteCache(key: string): void {
|
||||
this.memoryCache.delete(key);
|
||||
}
|
||||
}
|
||||
301
src/stores/groupListSources.ts
Normal file
301
src/stores/groupListSources.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
/**
|
||||
* 群组列表数据源抽象:游标、偏移分页实现同一契约,GroupManager 只依赖接口。
|
||||
*/
|
||||
|
||||
import {
|
||||
GroupResponse,
|
||||
GroupMemberResponse,
|
||||
GroupListResponse,
|
||||
GroupMemberListResponse,
|
||||
CursorPaginationRequest,
|
||||
} from '../types/dto';
|
||||
import { groupService } from '../services/groupService';
|
||||
import { getAllGroupsCache, getGroupMembersCache } from '../services/database';
|
||||
|
||||
export const GROUP_LIST_PAGE_SIZE = 20;
|
||||
export const GROUP_MEMBER_LIST_PAGE_SIZE = 50;
|
||||
|
||||
// ==================== 群组列表数据源 ====================
|
||||
|
||||
/** 单次拉取结果(一页或一批) */
|
||||
export interface GroupListPage {
|
||||
items: GroupResponse[];
|
||||
/** 在当前源上是否还能再 loadNext 一页 */
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页式群组列表源:restart 后开始新序列;首次 loadNext 为第一页,之后为后续页。
|
||||
*/
|
||||
export interface IGroupListPagedSource {
|
||||
restart(): void;
|
||||
loadNext(): Promise<GroupListPage>;
|
||||
/** 至少成功 loadNext 过一次且仍有下一页时为 true */
|
||||
readonly hasMore: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 远端群组列表(游标分页)
|
||||
*/
|
||||
export class NetworkCursorGroupListPagedSource implements IGroupListPagedSource {
|
||||
private nextCursor: string | null = null;
|
||||
private hasMoreAfterLastLoad = false;
|
||||
private loadedOnce = false;
|
||||
private readonly pageSize: number;
|
||||
|
||||
constructor(pageSize: number = GROUP_LIST_PAGE_SIZE) {
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
restart(): void {
|
||||
this.nextCursor = null;
|
||||
this.hasMoreAfterLastLoad = false;
|
||||
this.loadedOnce = false;
|
||||
}
|
||||
|
||||
get hasMore(): boolean {
|
||||
return this.loadedOnce && this.hasMoreAfterLastLoad;
|
||||
}
|
||||
|
||||
async loadNext(): Promise<GroupListPage> {
|
||||
const params: CursorPaginationRequest = {
|
||||
cursor: this.nextCursor ?? '',
|
||||
page_size: this.pageSize,
|
||||
};
|
||||
|
||||
const result = await groupService.getGroupsCursor(params);
|
||||
|
||||
this.hasMoreAfterLastLoad = result.has_more ?? false;
|
||||
this.nextCursor = result.next_cursor ?? null;
|
||||
this.loadedOnce = true;
|
||||
|
||||
return { items: result.list ?? [], hasMore: this.hasMoreAfterLastLoad };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 远端群组列表(偏移分页)
|
||||
*/
|
||||
export class NetworkOffsetGroupListPagedSource implements IGroupListPagedSource {
|
||||
private nextPage = 1;
|
||||
private hasMoreAfterLastLoad = false;
|
||||
private loadedOnce = false;
|
||||
private readonly pageSize: number;
|
||||
|
||||
constructor(pageSize: number = GROUP_LIST_PAGE_SIZE) {
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
restart(): void {
|
||||
this.nextPage = 1;
|
||||
this.hasMoreAfterLastLoad = false;
|
||||
this.loadedOnce = false;
|
||||
}
|
||||
|
||||
get hasMore(): boolean {
|
||||
return this.loadedOnce && this.hasMoreAfterLastLoad;
|
||||
}
|
||||
|
||||
async loadNext(): Promise<GroupListPage> {
|
||||
const result = await groupService.getGroups(this.nextPage, this.pageSize);
|
||||
|
||||
const items = result.list ?? [];
|
||||
const totalPages = result.total_pages ?? 1;
|
||||
|
||||
this.hasMoreAfterLastLoad = this.nextPage < totalPages;
|
||||
this.nextPage++;
|
||||
this.loadedOnce = true;
|
||||
|
||||
return { items, hasMore: this.hasMoreAfterLastLoad };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SQLite 群组列表缓存(单批,无后续页)
|
||||
*/
|
||||
export class SqliteGroupListPagedSource implements IGroupListPagedSource {
|
||||
private consumed = false;
|
||||
|
||||
restart(): void {
|
||||
this.consumed = false;
|
||||
}
|
||||
|
||||
get hasMore(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async loadNext(): Promise<GroupListPage> {
|
||||
if (this.consumed) {
|
||||
return { items: [], hasMore: false };
|
||||
}
|
||||
this.consumed = true;
|
||||
try {
|
||||
const items = await getAllGroupsCache();
|
||||
return { items, hasMore: false };
|
||||
} catch (e) {
|
||||
console.warn('[SqliteGroupListPagedSource] 读取群组列表缓存失败:', e);
|
||||
return { items: [], hasMore: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 群组成员列表数据源 ====================
|
||||
|
||||
/** 成员列表单次拉取结果 */
|
||||
export interface GroupMemberListPage {
|
||||
items: GroupMemberResponse[];
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页式群组成员列表源
|
||||
*/
|
||||
export interface IGroupMemberListPagedSource {
|
||||
restart(): void;
|
||||
loadNext(): Promise<GroupMemberListPage>;
|
||||
readonly hasMore: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 远端群组成员列表(游标分页)
|
||||
*/
|
||||
export class NetworkCursorGroupMemberListPagedSource implements IGroupMemberListPagedSource {
|
||||
private nextCursor: string | null = null;
|
||||
private hasMoreAfterLastLoad = false;
|
||||
private loadedOnce = false;
|
||||
private readonly pageSize: number;
|
||||
private readonly groupId: string;
|
||||
|
||||
constructor(groupId: string, pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE) {
|
||||
this.groupId = groupId;
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
restart(): void {
|
||||
this.nextCursor = null;
|
||||
this.hasMoreAfterLastLoad = false;
|
||||
this.loadedOnce = false;
|
||||
}
|
||||
|
||||
get hasMore(): boolean {
|
||||
return this.loadedOnce && this.hasMoreAfterLastLoad;
|
||||
}
|
||||
|
||||
async loadNext(): Promise<GroupMemberListPage> {
|
||||
const params: CursorPaginationRequest = {
|
||||
cursor: this.nextCursor ?? '',
|
||||
page_size: this.pageSize,
|
||||
};
|
||||
|
||||
const result = await groupService.getGroupMembersCursor(this.groupId, params);
|
||||
|
||||
this.hasMoreAfterLastLoad = result.has_more ?? false;
|
||||
this.nextCursor = result.next_cursor ?? null;
|
||||
this.loadedOnce = true;
|
||||
|
||||
return { items: result.list ?? [], hasMore: this.hasMoreAfterLastLoad };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 远端群组成员列表(偏移分页)
|
||||
*/
|
||||
export class NetworkOffsetGroupMemberListPagedSource implements IGroupMemberListPagedSource {
|
||||
private nextPage = 1;
|
||||
private hasMoreAfterLastLoad = false;
|
||||
private loadedOnce = false;
|
||||
private readonly pageSize: number;
|
||||
private readonly groupId: string;
|
||||
|
||||
constructor(groupId: string, pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE) {
|
||||
this.groupId = groupId;
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
restart(): void {
|
||||
this.nextPage = 1;
|
||||
this.hasMoreAfterLastLoad = false;
|
||||
this.loadedOnce = false;
|
||||
}
|
||||
|
||||
get hasMore(): boolean {
|
||||
return this.loadedOnce && this.hasMoreAfterLastLoad;
|
||||
}
|
||||
|
||||
async loadNext(): Promise<GroupMemberListPage> {
|
||||
const result = await groupService.getMembers(
|
||||
this.groupId,
|
||||
this.nextPage,
|
||||
this.pageSize
|
||||
);
|
||||
|
||||
const items = result.list ?? [];
|
||||
const totalPages = result.total_pages ?? 1;
|
||||
|
||||
this.hasMoreAfterLastLoad = this.nextPage < totalPages;
|
||||
this.nextPage++;
|
||||
this.loadedOnce = true;
|
||||
|
||||
return { items, hasMore: this.hasMoreAfterLastLoad };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SQLite 群组成员列表缓存(单批,无后续页)
|
||||
*/
|
||||
export class SqliteGroupMemberListPagedSource implements IGroupMemberListPagedSource {
|
||||
private consumed = false;
|
||||
private readonly groupId: string;
|
||||
|
||||
constructor(groupId: string) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
restart(): void {
|
||||
this.consumed = false;
|
||||
}
|
||||
|
||||
get hasMore(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async loadNext(): Promise<GroupMemberListPage> {
|
||||
if (this.consumed) {
|
||||
return { items: [], hasMore: false };
|
||||
}
|
||||
this.consumed = true;
|
||||
try {
|
||||
const items = await getGroupMembersCache(this.groupId);
|
||||
return { items, hasMore: false };
|
||||
} catch (e) {
|
||||
console.warn('[SqliteGroupMemberListPagedSource] 读取成员列表缓存失败:', e);
|
||||
return { items: [], hasMore: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工厂函数 ====================
|
||||
|
||||
/** 远端群组列表分页策略 */
|
||||
export type RemoteGroupListSourceKind = 'cursor' | 'offset';
|
||||
|
||||
export function createRemoteGroupListSource(
|
||||
kind: RemoteGroupListSourceKind = 'cursor',
|
||||
pageSize: number = GROUP_LIST_PAGE_SIZE
|
||||
): IGroupListPagedSource {
|
||||
if (kind === 'offset') {
|
||||
return new NetworkOffsetGroupListPagedSource(pageSize);
|
||||
}
|
||||
return new NetworkCursorGroupListPagedSource(pageSize);
|
||||
}
|
||||
|
||||
export function createRemoteGroupMemberListSource(
|
||||
groupId: string,
|
||||
kind: RemoteGroupListSourceKind = 'cursor',
|
||||
pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE
|
||||
): IGroupMemberListPagedSource {
|
||||
if (kind === 'offset') {
|
||||
return new NetworkOffsetGroupMemberListPagedSource(groupId, pageSize);
|
||||
}
|
||||
return new NetworkCursorGroupMemberListPagedSource(groupId, pageSize);
|
||||
}
|
||||
@@ -1,3 +1,11 @@
|
||||
/**
|
||||
* GroupManager - 群组管理核心模块
|
||||
*
|
||||
* 架构特点:
|
||||
* - 使用 CachedManager 基类,消除重复的缓存/去重逻辑
|
||||
* - 支持 Sources 模式进行分页加载
|
||||
*/
|
||||
|
||||
import {
|
||||
GroupListResponse,
|
||||
GroupMemberListResponse,
|
||||
@@ -14,7 +22,19 @@ import {
|
||||
saveGroupsCache,
|
||||
saveUsersCache,
|
||||
} from '../services/database';
|
||||
import { CacheBus, CacheEvent } from './cacheBus';
|
||||
import { CacheEvent } from './cacheBus';
|
||||
import { CachedManager, CacheEntry, createCacheEntry, isCacheExpired } from './baseManager';
|
||||
import {
|
||||
IGroupListPagedSource,
|
||||
IGroupMemberListPagedSource,
|
||||
createRemoteGroupListSource,
|
||||
createRemoteGroupMemberListSource,
|
||||
RemoteGroupListSourceKind,
|
||||
GROUP_LIST_PAGE_SIZE,
|
||||
GROUP_MEMBER_LIST_PAGE_SIZE,
|
||||
} from './groupListSources';
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
interface GroupManagerSnapshot {
|
||||
groupIds: string[];
|
||||
@@ -28,20 +48,18 @@ type GroupManagerEvent =
|
||||
| CacheEvent<GroupManagerSnapshot>
|
||||
| CacheEvent<{ error: unknown; context: string }>;
|
||||
|
||||
// ==================== 常量 ====================
|
||||
|
||||
const GROUP_TTL = 60 * 1000;
|
||||
const MEMBERS_TTL = 120 * 1000;
|
||||
|
||||
interface CacheEntry<T> {
|
||||
data: T;
|
||||
timestamp: number;
|
||||
ttl: number;
|
||||
}
|
||||
// ==================== Manager 实现 ====================
|
||||
|
||||
class GroupManager extends CacheBus<GroupManagerSnapshot, GroupManagerEvent> {
|
||||
private groupsListCache: CacheEntry<GroupResponse[]> | null = null;
|
||||
class GroupManager extends CachedManager<GroupManagerSnapshot, GroupManagerEvent> {
|
||||
/** 群组详情缓存 */
|
||||
private groupDetailCache = new Map<string, CacheEntry<GroupResponse | null>>();
|
||||
/** 群组成员缓存 */
|
||||
private groupMembersCache = new Map<string, CacheEntry<GroupMemberResponse[]>>();
|
||||
private pendingRequests = new Map<string, Promise<any>>();
|
||||
|
||||
protected getSnapshot(): GroupManagerSnapshot {
|
||||
return {
|
||||
@@ -50,67 +68,48 @@ class GroupManager extends CacheBus<GroupManagerSnapshot, GroupManagerEvent> {
|
||||
};
|
||||
}
|
||||
|
||||
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 getGroups(page = 1, pageSize = GROUP_LIST_PAGE_SIZE, forceRefresh = false): Promise<GroupListResponse> {
|
||||
const key = `list:${page}:${pageSize}`;
|
||||
|
||||
async getGroups(page = 1, pageSize = 20, forceRefresh = false): Promise<GroupListResponse> {
|
||||
if (page === 1 && !forceRefresh && this.groupsListCache && !this.isExpired(this.groupsListCache)) {
|
||||
return {
|
||||
list: this.groupsListCache.data,
|
||||
total: this.groupsListCache.data.length,
|
||||
page: 1,
|
||||
page_size: pageSize,
|
||||
total_pages: 1,
|
||||
};
|
||||
// 第一页且有有效缓存
|
||||
if (page === 1 && !forceRefresh && this.hasValidCache(key)) {
|
||||
const groups = this.getFromCache<GroupResponse[]>(key)!;
|
||||
return this.buildGroupListResponse(groups, pageSize);
|
||||
}
|
||||
|
||||
if (page === 1 && !forceRefresh && this.groupsListCache && this.isExpired(this.groupsListCache)) {
|
||||
// 第一页且缓存过期:后台刷新
|
||||
if (page === 1 && !forceRefresh && this.hasExpiredCache(key)) {
|
||||
this.refreshGroupsInBackground(page, pageSize);
|
||||
return {
|
||||
list: this.groupsListCache.data,
|
||||
total: this.groupsListCache.data.length,
|
||||
page: 1,
|
||||
page_size: pageSize,
|
||||
total_pages: 1,
|
||||
};
|
||||
const groups = this.getFromCache<GroupResponse[]>(key)!;
|
||||
return this.buildGroupListResponse(groups, pageSize);
|
||||
}
|
||||
|
||||
// 第一页:尝试本地缓存
|
||||
if (page === 1 && !forceRefresh) {
|
||||
const localGroups = await getAllGroupsCache();
|
||||
if (localGroups.length > 0) {
|
||||
this.groupsListCache = { data: localGroups, timestamp: Date.now(), ttl: GROUP_TTL };
|
||||
this.setCache(key, localGroups, GROUP_TTL);
|
||||
this.notify({
|
||||
type: 'list_updated',
|
||||
payload: { groups: localGroups },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
this.refreshGroupsInBackground(page, pageSize);
|
||||
return {
|
||||
list: localGroups,
|
||||
total: localGroups.length,
|
||||
page: 1,
|
||||
page_size: pageSize,
|
||||
total_pages: 1,
|
||||
};
|
||||
return this.buildGroupListResponse(localGroups, pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
// 发起请求
|
||||
return this.dedupe(`groups:list:${page}:${pageSize}`, async () => {
|
||||
const response = await groupService.fetchGroupsFromApi(page, pageSize);
|
||||
const response = await groupService.getGroups(page, pageSize);
|
||||
const groups = response.list || [];
|
||||
if (page === 1) {
|
||||
this.groupsListCache = { data: groups, timestamp: Date.now(), ttl: GROUP_TTL };
|
||||
this.setCache(key, groups, GROUP_TTL);
|
||||
}
|
||||
await saveGroupsCache(groups).catch(() => {});
|
||||
this.notify({
|
||||
@@ -122,12 +121,23 @@ class GroupManager extends CacheBus<GroupManagerSnapshot, GroupManagerEvent> {
|
||||
});
|
||||
}
|
||||
|
||||
private refreshGroupsInBackground(page = 1, pageSize = 20): void {
|
||||
this.dedupe(`groups:list:bg:${page}:${pageSize}`, async () => {
|
||||
const response = await groupService.fetchGroupsFromApi(page, pageSize);
|
||||
private buildGroupListResponse(groups: GroupResponse[], pageSize: number): GroupListResponse {
|
||||
return {
|
||||
list: groups,
|
||||
total: groups.length,
|
||||
page: 1,
|
||||
page_size: pageSize,
|
||||
total_pages: 1,
|
||||
};
|
||||
}
|
||||
|
||||
private refreshGroupsInBackground(page = 1, pageSize = GROUP_LIST_PAGE_SIZE): void {
|
||||
const key = `list:${page}:${pageSize}`;
|
||||
this.dedupe(`groups:bg:${key}`, async () => {
|
||||
const response = await groupService.getGroups(page, pageSize);
|
||||
const groups = response.list || [];
|
||||
if (page === 1) {
|
||||
this.groupsListCache = { data: groups, timestamp: Date.now(), ttl: GROUP_TTL };
|
||||
this.setCache(key, groups, GROUP_TTL);
|
||||
saveGroupsCache(groups).catch(() => {});
|
||||
}
|
||||
this.notify({
|
||||
@@ -145,39 +155,58 @@ class GroupManager extends CacheBus<GroupManagerSnapshot, GroupManagerEvent> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建群组列表数据源(用于 Sources 模式)
|
||||
*/
|
||||
createGroupListSource(
|
||||
kind: RemoteGroupListSourceKind = 'cursor',
|
||||
pageSize: number = GROUP_LIST_PAGE_SIZE
|
||||
): IGroupListPagedSource {
|
||||
return createRemoteGroupListSource(kind, pageSize);
|
||||
}
|
||||
|
||||
// ==================== 群组详情相关 ====================
|
||||
|
||||
/**
|
||||
* 获取群组详情
|
||||
*/
|
||||
async getGroup(groupId: string, forceRefresh = false): Promise<GroupResponse> {
|
||||
const id = groupId;
|
||||
const cached = this.groupDetailCache.get(id);
|
||||
if (!forceRefresh && cached && !this.isExpired(cached) && cached.data) {
|
||||
const cached = this.groupDetailCache.get(groupId);
|
||||
|
||||
// 有效缓存
|
||||
if (!forceRefresh && cached && !isCacheExpired(cached) && cached.data) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
if (!forceRefresh && cached && this.isExpired(cached) && cached.data) {
|
||||
this.refreshGroupInBackground(id);
|
||||
// 过期缓存:后台刷新
|
||||
if (!forceRefresh && cached && isCacheExpired(cached) && cached.data) {
|
||||
this.refreshGroupInBackground(groupId);
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
// 尝试本地缓存
|
||||
if (!forceRefresh) {
|
||||
const localGroup = await getGroupCache(id);
|
||||
const localGroup = await getGroupCache(groupId);
|
||||
if (localGroup) {
|
||||
this.groupDetailCache.set(id, { data: localGroup, timestamp: Date.now(), ttl: GROUP_TTL });
|
||||
this.groupDetailCache.set(groupId, createCacheEntry(localGroup, GROUP_TTL));
|
||||
this.notify({
|
||||
type: 'detail_updated',
|
||||
payload: { groupId: id, group: localGroup },
|
||||
payload: { groupId, group: localGroup },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
this.refreshGroupInBackground(id);
|
||||
this.refreshGroupInBackground(groupId);
|
||||
return localGroup;
|
||||
}
|
||||
}
|
||||
|
||||
return this.dedupe(`groups:detail:${id}`, async () => {
|
||||
const group = await groupService.fetchGroupFromApi(id);
|
||||
this.groupDetailCache.set(id, { data: group, timestamp: Date.now(), ttl: GROUP_TTL });
|
||||
// 发起请求
|
||||
return this.dedupe(`groups:detail:${groupId}`, async () => {
|
||||
const group = await groupService.getGroup(groupId);
|
||||
this.groupDetailCache.set(groupId, createCacheEntry(group, GROUP_TTL));
|
||||
await saveGroupCache(group).catch(() => {});
|
||||
this.notify({
|
||||
type: 'detail_updated',
|
||||
payload: { groupId: id, group },
|
||||
payload: { groupId, group },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
return group;
|
||||
@@ -186,8 +215,8 @@ class GroupManager extends CacheBus<GroupManagerSnapshot, GroupManagerEvent> {
|
||||
|
||||
private refreshGroupInBackground(groupId: string): void {
|
||||
this.dedupe(`groups:detail:bg:${groupId}`, async () => {
|
||||
const group = await groupService.fetchGroupFromApi(groupId);
|
||||
this.groupDetailCache.set(groupId, { data: group, timestamp: Date.now(), ttl: GROUP_TTL });
|
||||
const group = await groupService.getGroup(groupId);
|
||||
this.groupDetailCache.set(groupId, createCacheEntry(group, GROUP_TTL));
|
||||
saveGroupCache(group).catch(() => {});
|
||||
this.notify({
|
||||
type: 'detail_updated',
|
||||
@@ -204,61 +233,53 @@ class GroupManager extends CacheBus<GroupManagerSnapshot, GroupManagerEvent> {
|
||||
});
|
||||
}
|
||||
|
||||
async getMembers(groupId: string, page = 1, pageSize = 50, forceRefresh = false): Promise<GroupMemberListResponse> {
|
||||
const id = groupId;
|
||||
const key = `${id}:${page}:${pageSize}`;
|
||||
// ==================== 群组成员相关 ====================
|
||||
|
||||
/**
|
||||
* 获取群组成员列表
|
||||
*/
|
||||
async getMembers(
|
||||
groupId: string,
|
||||
page = 1,
|
||||
pageSize = GROUP_MEMBER_LIST_PAGE_SIZE,
|
||||
forceRefresh = false
|
||||
): Promise<GroupMemberListResponse> {
|
||||
const key = `members:${groupId}:${page}:${pageSize}`;
|
||||
const cached = this.groupMembersCache.get(key);
|
||||
if (!forceRefresh && cached && !this.isExpired(cached)) {
|
||||
return {
|
||||
list: cached.data,
|
||||
total: cached.data.length,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
total_pages: 1,
|
||||
};
|
||||
|
||||
// 有效缓存
|
||||
if (!forceRefresh && cached && !isCacheExpired(cached)) {
|
||||
return this.buildMemberListResponse(cached.data, page, pageSize);
|
||||
}
|
||||
|
||||
if (!forceRefresh && cached && this.isExpired(cached)) {
|
||||
this.refreshMembersInBackground(id, page, pageSize);
|
||||
return {
|
||||
list: cached.data,
|
||||
total: cached.data.length,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
total_pages: 1,
|
||||
};
|
||||
// 过期缓存:后台刷新
|
||||
if (!forceRefresh && cached && isCacheExpired(cached)) {
|
||||
this.refreshMembersInBackground(groupId, page, pageSize);
|
||||
return this.buildMemberListResponse(cached.data, page, pageSize);
|
||||
}
|
||||
|
||||
// 第一页:尝试本地缓存
|
||||
if (page === 1 && !forceRefresh) {
|
||||
const localMembers = await getGroupMembersCache(id);
|
||||
const localMembers = await getGroupMembersCache(groupId);
|
||||
if (localMembers.length > 0) {
|
||||
this.groupMembersCache.set(key, {
|
||||
data: localMembers,
|
||||
timestamp: Date.now(),
|
||||
ttl: MEMBERS_TTL,
|
||||
});
|
||||
this.groupMembersCache.set(key, createCacheEntry(localMembers, MEMBERS_TTL));
|
||||
this.notify({
|
||||
type: 'detail_updated',
|
||||
payload: { groupId: id, members: localMembers },
|
||||
payload: { groupId, members: localMembers },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
this.refreshMembersInBackground(id, page, pageSize);
|
||||
return {
|
||||
list: localMembers,
|
||||
total: localMembers.length,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
total_pages: 1,
|
||||
};
|
||||
this.refreshMembersInBackground(groupId, page, pageSize);
|
||||
return this.buildMemberListResponse(localMembers, page, pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
// 发起请求
|
||||
return this.dedupe(`groups:members:${key}`, async () => {
|
||||
const response = await groupService.fetchMembersFromApi(id, page, pageSize);
|
||||
const response = await groupService.getMembers(groupId, page, pageSize);
|
||||
const members = response.list || [];
|
||||
this.groupMembersCache.set(key, { data: members, timestamp: Date.now(), ttl: MEMBERS_TTL });
|
||||
this.groupMembersCache.set(key, createCacheEntry(members, MEMBERS_TTL));
|
||||
if (page === 1) {
|
||||
await saveGroupMembersCache(id, members).catch(() => {});
|
||||
await saveGroupMembersCache(groupId, members).catch(() => {});
|
||||
}
|
||||
await saveUsersCache(
|
||||
members
|
||||
@@ -267,19 +288,37 @@ class GroupManager extends CacheBus<GroupManagerSnapshot, GroupManagerEvent> {
|
||||
).catch(() => {});
|
||||
this.notify({
|
||||
type: 'detail_updated',
|
||||
payload: { groupId: id, members },
|
||||
payload: { groupId, members },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
private refreshMembersInBackground(groupId: string, page = 1, pageSize = 50): void {
|
||||
this.dedupe(`groups:members:bg:${groupId}:${page}:${pageSize}`, async () => {
|
||||
const response = await groupService.fetchMembersFromApi(groupId, page, pageSize);
|
||||
private buildMemberListResponse(
|
||||
members: GroupMemberResponse[],
|
||||
page: number,
|
||||
pageSize: number
|
||||
): GroupMemberListResponse {
|
||||
return {
|
||||
list: members,
|
||||
total: members.length,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
total_pages: 1,
|
||||
};
|
||||
}
|
||||
|
||||
private refreshMembersInBackground(
|
||||
groupId: string,
|
||||
page = 1,
|
||||
pageSize = GROUP_MEMBER_LIST_PAGE_SIZE
|
||||
): void {
|
||||
const key = `members:${groupId}:${page}:${pageSize}`;
|
||||
this.dedupe(`groups:members:bg:${key}`, async () => {
|
||||
const response = await groupService.getMembers(groupId, page, pageSize);
|
||||
const members = response.list || [];
|
||||
const key = `${groupId}:${page}:${pageSize}`;
|
||||
this.groupMembersCache.set(key, { data: members, timestamp: Date.now(), ttl: MEMBERS_TTL });
|
||||
this.groupMembersCache.set(key, createCacheEntry(members, MEMBERS_TTL));
|
||||
if (page === 1) {
|
||||
saveGroupMembersCache(groupId, members).catch(() => {});
|
||||
}
|
||||
@@ -303,21 +342,46 @@ class GroupManager extends CacheBus<GroupManagerSnapshot, GroupManagerEvent> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建群组成员列表数据源(用于 Sources 模式)
|
||||
*/
|
||||
createGroupMemberListSource(
|
||||
groupId: string,
|
||||
kind: RemoteGroupListSourceKind = 'cursor',
|
||||
pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE
|
||||
): IGroupMemberListPagedSource {
|
||||
return createRemoteGroupMemberListSource(groupId, kind, pageSize);
|
||||
}
|
||||
|
||||
// ==================== 缓存管理 ====================
|
||||
|
||||
/**
|
||||
* 使缓存失效
|
||||
*/
|
||||
invalidate(groupId?: string): void {
|
||||
if (!groupId) {
|
||||
this.groupsListCache = null;
|
||||
this.clearCache();
|
||||
this.groupDetailCache.clear();
|
||||
this.groupMembersCache.clear();
|
||||
return;
|
||||
}
|
||||
this.groupDetailCache.delete(groupId);
|
||||
[...this.groupMembersCache.keys()].forEach((key) => {
|
||||
if (key.startsWith(`${groupId}:`)) {
|
||||
if (key.startsWith(`members:${groupId}:`)) {
|
||||
this.groupMembersCache.delete(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有数据
|
||||
*/
|
||||
clear(): void {
|
||||
this.clearCache();
|
||||
this.groupDetailCache.clear();
|
||||
this.groupMembersCache.clear();
|
||||
this.clearPendingRequests();
|
||||
}
|
||||
}
|
||||
|
||||
export const groupManager = new GroupManager();
|
||||
|
||||
|
||||
137
src/stores/postListSources.ts
Normal file
137
src/stores/postListSources.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 帖子列表数据源抽象:游标、偏移分页实现同一契约,PostManager 只依赖接口。
|
||||
*/
|
||||
|
||||
import { Post } from '../types';
|
||||
import { postService } from '../services/postService';
|
||||
import type { CursorPaginationRequest } from '../types/dto';
|
||||
|
||||
export const POST_LIST_PAGE_SIZE = 20;
|
||||
|
||||
/** 帖子列表类型 */
|
||||
export type PostListTab = 'hot' | 'latest' | 'follow';
|
||||
|
||||
/** 单次拉取结果(一页或一批) */
|
||||
export interface PostListPage {
|
||||
items: Post[];
|
||||
/** 在当前源上是否还能再 loadNext 一页 */
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页式帖子列表源:restart 后开始新序列;首次 loadNext 为第一页,之后为后续页。
|
||||
*/
|
||||
export interface IPostListPagedSource {
|
||||
restart(): void;
|
||||
loadNext(): Promise<PostListPage>;
|
||||
/** 至少成功 loadNext 过一次且仍有下一页时为 true */
|
||||
readonly hasMore: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 远端帖子列表(游标分页)
|
||||
*/
|
||||
export class NetworkCursorPostListPagedSource implements IPostListPagedSource {
|
||||
private nextCursor: string | null = null;
|
||||
private hasMoreAfterLastLoad = false;
|
||||
private loadedOnce = false;
|
||||
private readonly pageSize: number;
|
||||
private readonly tab?: PostListTab;
|
||||
private readonly channelId?: string;
|
||||
|
||||
constructor(
|
||||
options: { tab?: PostListTab; channelId?: string; pageSize?: number } = {}
|
||||
) {
|
||||
this.tab = options.tab;
|
||||
this.channelId = options.channelId;
|
||||
this.pageSize = options.pageSize ?? POST_LIST_PAGE_SIZE;
|
||||
}
|
||||
|
||||
restart(): void {
|
||||
this.nextCursor = null;
|
||||
this.hasMoreAfterLastLoad = false;
|
||||
this.loadedOnce = false;
|
||||
}
|
||||
|
||||
get hasMore(): boolean {
|
||||
return this.loadedOnce && this.hasMoreAfterLastLoad;
|
||||
}
|
||||
|
||||
async loadNext(): Promise<PostListPage> {
|
||||
const params: CursorPaginationRequest = {
|
||||
cursor: this.nextCursor ?? '',
|
||||
page_size: this.pageSize,
|
||||
post_type: this.tab,
|
||||
};
|
||||
|
||||
const result = await postService.getPostsCursor(params);
|
||||
|
||||
this.hasMoreAfterLastLoad = result.has_more ?? false;
|
||||
this.nextCursor = result.next_cursor ?? null;
|
||||
this.loadedOnce = true;
|
||||
|
||||
return { items: result.list ?? [], hasMore: this.hasMoreAfterLastLoad };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 远端帖子列表(偏移分页)
|
||||
*/
|
||||
export class NetworkOffsetPostListPagedSource implements IPostListPagedSource {
|
||||
private nextPage = 1;
|
||||
private hasMoreAfterLastLoad = false;
|
||||
private loadedOnce = false;
|
||||
private readonly pageSize: number;
|
||||
private readonly tab?: PostListTab;
|
||||
private readonly channelId?: string;
|
||||
|
||||
constructor(
|
||||
options: { tab?: PostListTab; channelId?: string; pageSize?: number } = {}
|
||||
) {
|
||||
this.tab = options.tab;
|
||||
this.channelId = options.channelId;
|
||||
this.pageSize = options.pageSize ?? POST_LIST_PAGE_SIZE;
|
||||
}
|
||||
|
||||
restart(): void {
|
||||
this.nextPage = 1;
|
||||
this.hasMoreAfterLastLoad = false;
|
||||
this.loadedOnce = false;
|
||||
}
|
||||
|
||||
get hasMore(): boolean {
|
||||
return this.loadedOnce && this.hasMoreAfterLastLoad;
|
||||
}
|
||||
|
||||
async loadNext(): Promise<PostListPage> {
|
||||
const result = await postService.getPosts(
|
||||
this.nextPage,
|
||||
this.pageSize,
|
||||
this.tab,
|
||||
this.channelId
|
||||
);
|
||||
|
||||
const items = result.list ?? [];
|
||||
const total = result.total ?? 0;
|
||||
const totalPages = result.total_pages ?? 1;
|
||||
|
||||
this.hasMoreAfterLastLoad = this.nextPage < totalPages;
|
||||
this.nextPage++;
|
||||
this.loadedOnce = true;
|
||||
|
||||
return { items, hasMore: this.hasMoreAfterLastLoad };
|
||||
}
|
||||
}
|
||||
|
||||
/** 远端帖子列表分页策略 */
|
||||
export type RemotePostListSourceKind = 'cursor' | 'offset';
|
||||
|
||||
export function createRemotePostListSource(
|
||||
kind: RemotePostListSourceKind = 'cursor',
|
||||
options: { tab?: PostListTab; channelId?: string; pageSize?: number } = {}
|
||||
): IPostListPagedSource {
|
||||
if (kind === 'offset') {
|
||||
return new NetworkOffsetPostListPagedSource(options);
|
||||
}
|
||||
return new NetworkCursorPostListPagedSource(options);
|
||||
}
|
||||
@@ -1,6 +1,24 @@
|
||||
/**
|
||||
* PostManager - 帖子管理核心模块
|
||||
*
|
||||
* 架构特点:
|
||||
* - 使用 CachedManager 基类,消除重复的缓存/去重逻辑
|
||||
* - 支持 Sources 模式进行分页加载
|
||||
*/
|
||||
|
||||
import { Post } from '../types';
|
||||
import { postService } from '../services/postService';
|
||||
import { CacheBus, CacheEvent } from './cacheBus';
|
||||
import { CacheEvent } from './cacheBus';
|
||||
import { CachedManager, CacheEntry, createCacheEntry, isCacheExpired } from './baseManager';
|
||||
import {
|
||||
IPostListPagedSource,
|
||||
PostListTab,
|
||||
createRemotePostListSource,
|
||||
RemotePostListSourceKind,
|
||||
POST_LIST_PAGE_SIZE,
|
||||
} from './postListSources';
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
interface PostListPayload {
|
||||
key: string;
|
||||
@@ -23,66 +41,57 @@ type PostManagerEvent =
|
||||
| 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;
|
||||
}
|
||||
// ==================== Manager 实现 ====================
|
||||
|
||||
class PostManager extends CacheBus<PostManagerSnapshot, PostManagerEvent> {
|
||||
private listCache = new Map<string, CacheEntry<Post[]>>();
|
||||
class PostManager extends CachedManager<PostManagerSnapshot, PostManagerEvent> {
|
||||
/** 详情缓存 */
|
||||
private detailCache = new Map<string, CacheEntry<Post | null>>();
|
||||
private pendingRequests = new Map<string, Promise<any>>();
|
||||
|
||||
protected getSnapshot(): PostManagerSnapshot {
|
||||
return {
|
||||
listKeys: [...this.listCache.keys()],
|
||||
listKeys: [...this.memoryCache.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}`;
|
||||
return `list:${type}:${page}:${pageSize}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取帖子列表(传统分页模式)
|
||||
*/
|
||||
async getPosts(
|
||||
type = 'hot',
|
||||
type: PostListTab = 'hot',
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
pageSize = POST_LIST_PAGE_SIZE,
|
||||
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 && this.hasValidCache(key)) {
|
||||
return this.getFromCache<Post[]>(key)!;
|
||||
}
|
||||
|
||||
if (!forceRefresh && cached && this.isExpired(cached)) {
|
||||
// 过期缓存:后台刷新,立即返回旧数据
|
||||
if (!forceRefresh && this.hasExpiredCache(key)) {
|
||||
this.refreshPostsInBackground(key, type, page, pageSize);
|
||||
return cached.data;
|
||||
return this.getFromCache<Post[]>(key)!;
|
||||
}
|
||||
|
||||
return this.dedupe(`posts:list:${key}`, async () => {
|
||||
// 无缓存:发起请求
|
||||
return this.dedupe(`posts:${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.setCache(key, posts, LIST_TTL);
|
||||
this.notify({
|
||||
type: 'list_updated',
|
||||
payload: { key, posts },
|
||||
@@ -92,11 +101,16 @@ class PostManager extends CacheBus<PostManagerSnapshot, PostManagerEvent> {
|
||||
});
|
||||
}
|
||||
|
||||
private refreshPostsInBackground(key: string, type: string, page: number, pageSize: number): void {
|
||||
this.dedupe(`posts:list:bg:${key}`, async () => {
|
||||
private refreshPostsInBackground(
|
||||
key: string,
|
||||
type: PostListTab,
|
||||
page: number,
|
||||
pageSize: number
|
||||
): void {
|
||||
this.dedupe(`posts: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.setCache(key, posts, LIST_TTL);
|
||||
this.notify({
|
||||
type: 'list_updated',
|
||||
payload: { key, posts },
|
||||
@@ -112,20 +126,39 @@ class PostManager extends CacheBus<PostManagerSnapshot, PostManagerEvent> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建帖子列表数据源(用于 Sources 模式)
|
||||
*/
|
||||
createPostListSource(
|
||||
options: { tab?: PostListTab; channelId?: string; pageSize?: number } = {},
|
||||
kind: RemotePostListSourceKind = 'cursor'
|
||||
): IPostListPagedSource {
|
||||
return createRemotePostListSource(kind, options);
|
||||
}
|
||||
|
||||
// ==================== 详情相关 ====================
|
||||
|
||||
/**
|
||||
* 获取帖子详情
|
||||
*/
|
||||
async getPostDetail(postId: string, forceRefresh = false): Promise<Post | null> {
|
||||
const cached = this.detailCache.get(postId);
|
||||
if (!forceRefresh && cached && !this.isExpired(cached)) {
|
||||
|
||||
// 有效缓存
|
||||
if (!forceRefresh && cached && !isCacheExpired(cached)) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
if (!forceRefresh && cached && this.isExpired(cached)) {
|
||||
// 过期缓存:后台刷新
|
||||
if (!forceRefresh && cached && isCacheExpired(cached) && cached.data) {
|
||||
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.detailCache.set(postId, createCacheEntry(post, DETAIL_TTL));
|
||||
this.notify({
|
||||
type: 'detail_updated',
|
||||
payload: { postId, post },
|
||||
@@ -138,7 +171,7 @@ class PostManager extends CacheBus<PostManagerSnapshot, PostManagerEvent> {
|
||||
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.detailCache.set(postId, createCacheEntry(post, DETAIL_TTL));
|
||||
this.notify({
|
||||
type: 'detail_updated',
|
||||
payload: { postId, post },
|
||||
@@ -154,21 +187,28 @@ class PostManager extends CacheBus<PostManagerSnapshot, PostManagerEvent> {
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 缓存管理 ====================
|
||||
|
||||
/**
|
||||
* 使缓存失效
|
||||
*/
|
||||
invalidate(postId?: string): void {
|
||||
if (postId) {
|
||||
this.detailCache.delete(postId);
|
||||
return;
|
||||
}
|
||||
this.listCache.clear();
|
||||
this.clearCache();
|
||||
this.detailCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有数据
|
||||
*/
|
||||
clear(): void {
|
||||
this.listCache.clear();
|
||||
this.clearCache();
|
||||
this.detailCache.clear();
|
||||
this.pendingRequests.clear();
|
||||
this.clearPendingRequests();
|
||||
}
|
||||
}
|
||||
|
||||
export const postManager = new PostManager();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user