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:
@@ -1,418 +0,0 @@
|
|||||||
/**
|
|
||||||
* 学习资料 Mock 数据
|
|
||||||
*/
|
|
||||||
import type { MaterialSubject, MaterialFile, MaterialFileType, SUBJECT_COLORS } from '../../types/material';
|
|
||||||
|
|
||||||
// 学科分类 Mock 数据
|
|
||||||
export const MOCK_SUBJECTS: MaterialSubject[] = [
|
|
||||||
{
|
|
||||||
id: 'subject-math',
|
|
||||||
name: '数学',
|
|
||||||
icon: 'calculator-variant',
|
|
||||||
color: '#FF6B6B',
|
|
||||||
description: '高等数学、线性代数、概率论等',
|
|
||||||
sort_order: 1,
|
|
||||||
material_count: 15,
|
|
||||||
created_at: '2024-01-01T00:00:00Z',
|
|
||||||
updated_at: '2024-03-20T10:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'subject-english',
|
|
||||||
name: '英语',
|
|
||||||
icon: 'translate',
|
|
||||||
color: '#4ECDC4',
|
|
||||||
description: '四六级、考研英语、托福雅思等',
|
|
||||||
sort_order: 2,
|
|
||||||
material_count: 12,
|
|
||||||
created_at: '2024-01-01T00:00:00Z',
|
|
||||||
updated_at: '2024-03-18T14:30:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'subject-physics',
|
|
||||||
name: '物理',
|
|
||||||
icon: 'atom',
|
|
||||||
color: '#45B7D1',
|
|
||||||
description: '大学物理、电磁学、力学等',
|
|
||||||
sort_order: 3,
|
|
||||||
material_count: 8,
|
|
||||||
created_at: '2024-01-01T00:00:00Z',
|
|
||||||
updated_at: '2024-03-15T09:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'subject-chemistry',
|
|
||||||
name: '化学',
|
|
||||||
icon: 'flask',
|
|
||||||
color: '#96CEB4',
|
|
||||||
description: '有机化学、无机化学、分析化学等',
|
|
||||||
sort_order: 4,
|
|
||||||
material_count: 6,
|
|
||||||
created_at: '2024-01-01T00:00:00Z',
|
|
||||||
updated_at: '2024-03-10T16:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'subject-cs',
|
|
||||||
name: '计算机',
|
|
||||||
icon: 'laptop',
|
|
||||||
color: '#FFEAA7',
|
|
||||||
description: '数据结构、算法、操作系统等',
|
|
||||||
sort_order: 5,
|
|
||||||
material_count: 20,
|
|
||||||
created_at: '2024-01-01T00:00:00Z',
|
|
||||||
updated_at: '2024-03-22T11:30:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'subject-economics',
|
|
||||||
name: '经济管理',
|
|
||||||
icon: 'chart-line',
|
|
||||||
color: '#DDA0DD',
|
|
||||||
description: '微观经济学、宏观经济学、管理学等',
|
|
||||||
sort_order: 6,
|
|
||||||
material_count: 10,
|
|
||||||
created_at: '2024-01-01T00:00:00Z',
|
|
||||||
updated_at: '2024-03-19T08:45:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'subject-law',
|
|
||||||
name: '法学',
|
|
||||||
icon: 'scale-balance',
|
|
||||||
color: '#F39C12',
|
|
||||||
description: '民法、刑法、宪法等',
|
|
||||||
sort_order: 7,
|
|
||||||
material_count: 7,
|
|
||||||
created_at: '2024-01-01T00:00:00Z',
|
|
||||||
updated_at: '2024-03-12T13:20:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'subject-chinese',
|
|
||||||
name: '语文文学',
|
|
||||||
icon: 'book-open-page-variant',
|
|
||||||
color: '#3498DB',
|
|
||||||
description: '古代文学、现代文学、写作等',
|
|
||||||
sort_order: 8,
|
|
||||||
material_count: 5,
|
|
||||||
created_at: '2024-01-01T00:00:00Z',
|
|
||||||
updated_at: '2024-03-08T15:00:00Z',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 文件资料 Mock 数据
|
|
||||||
export const MOCK_MATERIALS: MaterialFile[] = [
|
|
||||||
// 数学
|
|
||||||
{
|
|
||||||
id: 'math-001',
|
|
||||||
subject_id: 'subject-math',
|
|
||||||
title: '高等数学上册重点笔记',
|
|
||||||
description: '包含极限、导数、积分等重点章节的详细笔记,配有典型例题解析',
|
|
||||||
file_type: 'pdf',
|
|
||||||
file_size: 5242880, // 5MB
|
|
||||||
file_url: 'https://example.com/materials/math/advanced_math_1.pdf',
|
|
||||||
file_name: '高等数学上册重点笔记.pdf',
|
|
||||||
download_count: 1256,
|
|
||||||
author_name: '张同学',
|
|
||||||
tags: ['高等数学', '期末复习', '重点笔记'],
|
|
||||||
created_at: '2024-01-15T10:00:00Z',
|
|
||||||
updated_at: '2024-02-20T14:30:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'math-002',
|
|
||||||
subject_id: 'subject-math',
|
|
||||||
title: '线性代数公式大全',
|
|
||||||
description: '线性代数所有公式汇总,适合考前突击复习',
|
|
||||||
file_type: 'pdf',
|
|
||||||
file_size: 1572864, // 1.5MB
|
|
||||||
file_url: 'https://example.com/materials/math/linear_algebra_formulas.pdf',
|
|
||||||
file_name: '线性代数公式大全.pdf',
|
|
||||||
download_count: 2341,
|
|
||||||
author_name: '李同学',
|
|
||||||
tags: ['线性代数', '公式', '复习'],
|
|
||||||
created_at: '2024-01-20T09:00:00Z',
|
|
||||||
updated_at: '2024-01-20T09:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'math-003',
|
|
||||||
subject_id: 'subject-math',
|
|
||||||
title: '概率论与数理统计课件',
|
|
||||||
description: '概率论课程完整PPT课件,包含所有章节',
|
|
||||||
file_type: 'ppt',
|
|
||||||
file_size: 15728640, // 15MB
|
|
||||||
file_url: 'https://example.com/materials/math/probability_slides.pptx',
|
|
||||||
file_name: '概率论与数理统计课件.pptx',
|
|
||||||
download_count: 876,
|
|
||||||
author_name: '王老师',
|
|
||||||
tags: ['概率论', '课件', 'PPT'],
|
|
||||||
created_at: '2024-02-01T08:00:00Z',
|
|
||||||
updated_at: '2024-02-15T10:00:00Z',
|
|
||||||
},
|
|
||||||
// 英语
|
|
||||||
{
|
|
||||||
id: 'english-001',
|
|
||||||
subject_id: 'subject-english',
|
|
||||||
title: '四级核心词汇手册',
|
|
||||||
description: '精选四级核心词汇2000个,附带例句和记忆技巧',
|
|
||||||
file_type: 'pdf',
|
|
||||||
file_size: 3145728, // 3MB
|
|
||||||
file_url: 'https://example.com/materials/english/cet4_vocabulary.pdf',
|
|
||||||
file_name: '四级核心词汇手册.pdf',
|
|
||||||
download_count: 3567,
|
|
||||||
author_name: '刘同学',
|
|
||||||
tags: ['四级', '词汇', '备考'],
|
|
||||||
created_at: '2024-01-10T11:00:00Z',
|
|
||||||
updated_at: '2024-02-28T16:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'english-002',
|
|
||||||
subject_id: 'subject-english',
|
|
||||||
title: '六级阅读理解技巧总结',
|
|
||||||
description: '六级阅读理解题型分析与解题技巧,包含真题解析',
|
|
||||||
file_type: 'word',
|
|
||||||
file_size: 2097152, // 2MB
|
|
||||||
file_url: 'https://example.com/materials/english/cet6_reading.docx',
|
|
||||||
file_name: '六级阅读理解技巧总结.docx',
|
|
||||||
download_count: 1892,
|
|
||||||
author_name: '陈同学',
|
|
||||||
tags: ['六级', '阅读理解', '技巧'],
|
|
||||||
created_at: '2024-01-25T14:00:00Z',
|
|
||||||
updated_at: '2024-01-25T14:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'english-003',
|
|
||||||
subject_id: 'subject-english',
|
|
||||||
title: '考研英语作文模板',
|
|
||||||
description: '考研英语大作文、小作文模板合集,覆盖常考话题',
|
|
||||||
file_type: 'pdf',
|
|
||||||
file_size: 1048576, // 1MB
|
|
||||||
file_url: 'https://example.com/materials/english/kaoyan_writing.pdf',
|
|
||||||
file_name: '考研英语作文模板.pdf',
|
|
||||||
download_count: 4521,
|
|
||||||
author_name: '周同学',
|
|
||||||
tags: ['考研', '英语', '作文'],
|
|
||||||
created_at: '2024-02-05T10:30:00Z',
|
|
||||||
updated_at: '2024-03-10T09:00:00Z',
|
|
||||||
},
|
|
||||||
// 物理
|
|
||||||
{
|
|
||||||
id: 'physics-001',
|
|
||||||
subject_id: 'subject-physics',
|
|
||||||
title: '大学物理期末复习提纲',
|
|
||||||
description: '大学物理上下册复习要点,包含公式总结和典型例题',
|
|
||||||
file_type: 'pdf',
|
|
||||||
file_size: 4194304, // 4MB
|
|
||||||
file_url: 'https://example.com/materials/physics/university_physics_review.pdf',
|
|
||||||
file_name: '大学物理期末复习提纲.pdf',
|
|
||||||
download_count: 987,
|
|
||||||
author_name: '吴同学',
|
|
||||||
tags: ['大学物理', '复习', '期末'],
|
|
||||||
created_at: '2024-01-18T15:00:00Z',
|
|
||||||
updated_at: '2024-02-22T11:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'physics-002',
|
|
||||||
subject_id: 'subject-physics',
|
|
||||||
title: '电磁学实验报告模板',
|
|
||||||
description: '电磁学实验报告标准格式和填写示例',
|
|
||||||
file_type: 'word',
|
|
||||||
file_size: 524288, // 512KB
|
|
||||||
file_url: 'https://example.com/materials/physics/em_lab_template.docx',
|
|
||||||
file_name: '电磁学实验报告模板.docx',
|
|
||||||
download_count: 654,
|
|
||||||
author_name: '实验室',
|
|
||||||
tags: ['电磁学', '实验', '报告'],
|
|
||||||
created_at: '2024-02-10T09:00:00Z',
|
|
||||||
updated_at: '2024-02-10T09:00:00Z',
|
|
||||||
},
|
|
||||||
// 化学
|
|
||||||
{
|
|
||||||
id: 'chemistry-001',
|
|
||||||
subject_id: 'subject-chemistry',
|
|
||||||
title: '有机化学反应机理汇总',
|
|
||||||
description: '有机化学常见反应机理图文详解',
|
|
||||||
file_type: 'pdf',
|
|
||||||
file_size: 8388608, // 8MB
|
|
||||||
file_url: 'https://example.com/materials/chemistry/organic_mechanisms.pdf',
|
|
||||||
file_name: '有机化学反应机理汇总.pdf',
|
|
||||||
download_count: 1123,
|
|
||||||
author_name: '赵同学',
|
|
||||||
tags: ['有机化学', '反应机理', '重点'],
|
|
||||||
created_at: '2024-01-22T10:00:00Z',
|
|
||||||
updated_at: '2024-02-18T14:00:00Z',
|
|
||||||
},
|
|
||||||
// 计算机
|
|
||||||
{
|
|
||||||
id: 'cs-001',
|
|
||||||
subject_id: 'subject-cs',
|
|
||||||
title: '数据结构与算法笔记',
|
|
||||||
description: '数据结构核心内容整理,包含时间复杂度分析和代码实现',
|
|
||||||
file_type: 'pdf',
|
|
||||||
file_size: 7340032, // 7MB
|
|
||||||
file_url: 'https://example.com/materials/cs/data_structures.pdf',
|
|
||||||
file_name: '数据结构与算法笔记.pdf',
|
|
||||||
download_count: 5678,
|
|
||||||
author_name: '孙同学',
|
|
||||||
tags: ['数据结构', '算法', '笔记'],
|
|
||||||
created_at: '2024-01-05T08:00:00Z',
|
|
||||||
updated_at: '2024-03-15T10:30:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'cs-002',
|
|
||||||
subject_id: 'subject-cs',
|
|
||||||
title: '操作系统概念详解',
|
|
||||||
description: '操作系统核心概念讲解,进程、内存、文件系统等',
|
|
||||||
file_type: 'pdf',
|
|
||||||
file_size: 6291456, // 6MB
|
|
||||||
file_url: 'https://example.com/materials/cs/operating_systems.pdf',
|
|
||||||
file_name: '操作系统概念详解.pdf',
|
|
||||||
download_count: 3421,
|
|
||||||
author_name: '钱同学',
|
|
||||||
tags: ['操作系统', '计算机', '笔记'],
|
|
||||||
created_at: '2024-01-12T09:30:00Z',
|
|
||||||
updated_at: '2024-02-25T16:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'cs-003',
|
|
||||||
subject_id: 'subject-cs',
|
|
||||||
title: '计算机网络课件',
|
|
||||||
description: '计算机网络课程完整PPT,涵盖TCP/IP协议栈',
|
|
||||||
file_type: 'ppt',
|
|
||||||
file_size: 20971520, // 20MB
|
|
||||||
file_url: 'https://example.com/materials/cs/network_slides.pptx',
|
|
||||||
file_name: '计算机网络课件.pptx',
|
|
||||||
download_count: 2134,
|
|
||||||
author_name: '计算机学院',
|
|
||||||
tags: ['计算机网络', '课件', 'PPT'],
|
|
||||||
created_at: '2024-02-08T10:00:00Z',
|
|
||||||
updated_at: '2024-02-28T09:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'cs-004',
|
|
||||||
subject_id: 'subject-cs',
|
|
||||||
title: 'LeetCode刷题笔记',
|
|
||||||
description: 'LeetCode热门题目解题思路和代码实现',
|
|
||||||
file_type: 'pdf',
|
|
||||||
file_size: 10485760, // 10MB
|
|
||||||
file_url: 'https://example.com/materials/cs/leetcode_notes.pdf',
|
|
||||||
file_name: 'LeetCode刷题笔记.pdf',
|
|
||||||
download_count: 8765,
|
|
||||||
author_name: '郑同学',
|
|
||||||
tags: ['算法', '面试', 'LeetCode'],
|
|
||||||
created_at: '2024-01-28T14:00:00Z',
|
|
||||||
updated_at: '2024-03-20T11:00:00Z',
|
|
||||||
},
|
|
||||||
// 经济管理
|
|
||||||
{
|
|
||||||
id: 'economics-001',
|
|
||||||
subject_id: 'subject-economics',
|
|
||||||
title: '微观经济学重点总结',
|
|
||||||
description: '微观经济学核心理论和图形分析',
|
|
||||||
file_type: 'pdf',
|
|
||||||
file_size: 3145728, // 3MB
|
|
||||||
file_url: 'https://example.com/materials/economics/microeconomics.pdf',
|
|
||||||
file_name: '微观经济学重点总结.pdf',
|
|
||||||
download_count: 1456,
|
|
||||||
author_name: '冯同学',
|
|
||||||
tags: ['微观经济学', '重点', '复习'],
|
|
||||||
created_at: '2024-01-30T11:00:00Z',
|
|
||||||
updated_at: '2024-02-20T15:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'economics-002',
|
|
||||||
subject_id: 'subject-economics',
|
|
||||||
title: '管理学案例分析模板',
|
|
||||||
description: '管理学课程案例分析报告格式和范例',
|
|
||||||
file_type: 'word',
|
|
||||||
file_size: 1572864, // 1.5MB
|
|
||||||
file_url: 'https://example.com/materials/economics/management_case.docx',
|
|
||||||
file_name: '管理学案例分析模板.docx',
|
|
||||||
download_count: 876,
|
|
||||||
author_name: '商学院',
|
|
||||||
tags: ['管理学', '案例分析', '模板'],
|
|
||||||
created_at: '2024-02-12T10:00:00Z',
|
|
||||||
updated_at: '2024-02-12T10:00:00Z',
|
|
||||||
},
|
|
||||||
// 法学
|
|
||||||
{
|
|
||||||
id: 'law-001',
|
|
||||||
subject_id: 'subject-law',
|
|
||||||
title: '民法总则笔记',
|
|
||||||
description: '民法总则核心知识点梳理',
|
|
||||||
file_type: 'pdf',
|
|
||||||
file_size: 2621440, // 2.5MB
|
|
||||||
file_url: 'https://example.com/materials/law/civil_law.pdf',
|
|
||||||
file_name: '民法总则笔记.pdf',
|
|
||||||
download_count: 765,
|
|
||||||
author_name: '何同学',
|
|
||||||
tags: ['民法', '笔记', '法学'],
|
|
||||||
created_at: '2024-02-03T09:00:00Z',
|
|
||||||
updated_at: '2024-02-15T14:00:00Z',
|
|
||||||
},
|
|
||||||
// 语文文学
|
|
||||||
{
|
|
||||||
id: 'chinese-001',
|
|
||||||
subject_id: 'subject-chinese',
|
|
||||||
title: '古代文学作品选读',
|
|
||||||
description: '中国古代文学经典篇目赏析',
|
|
||||||
file_type: 'pdf',
|
|
||||||
file_size: 5242880, // 5MB
|
|
||||||
file_url: 'https://example.com/materials/chinese/ancient_literature.pdf',
|
|
||||||
file_name: '古代文学作品选读.pdf',
|
|
||||||
download_count: 543,
|
|
||||||
author_name: '中文系',
|
|
||||||
tags: ['古代文学', '作品选', '赏析'],
|
|
||||||
created_at: '2024-02-18T14:00:00Z',
|
|
||||||
updated_at: '2024-02-18T14:00:00Z',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 获取学科列表
|
|
||||||
export function getMockSubjects(): Promise<MaterialSubject[]> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve([...MOCK_SUBJECTS].sort((a, b) => a.sort_order - b.sort_order));
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据学科ID获取资料列表
|
|
||||||
export function getMockMaterialsBySubject(subjectId: string): Promise<MaterialFile[]> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const materials = MOCK_MATERIALS.filter((m) => m.subject_id === subjectId);
|
|
||||||
resolve(materials);
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索资料
|
|
||||||
export function searchMockMaterials(keyword: string): Promise<MaterialFile[]> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const lowerKeyword = keyword.toLowerCase();
|
|
||||||
const materials = MOCK_MATERIALS.filter(
|
|
||||||
(m) =>
|
|
||||||
m.title.toLowerCase().includes(lowerKeyword) ||
|
|
||||||
m.description?.toLowerCase().includes(lowerKeyword) ||
|
|
||||||
m.tags?.some((t) => t.toLowerCase().includes(lowerKeyword))
|
|
||||||
);
|
|
||||||
resolve(materials);
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据ID获取资料详情
|
|
||||||
export function getMockMaterialById(id: string): Promise<MaterialFile | null> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const material = MOCK_MATERIALS.find((m) => m.id === id);
|
|
||||||
resolve(material || null);
|
|
||||||
}, 200);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据ID获取学科详情
|
|
||||||
export function getMockSubjectById(id: string): Promise<MaterialSubject | null> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const subject = MOCK_SUBJECTS.find((s) => s.id === id);
|
|
||||||
resolve(subject || null);
|
|
||||||
}, 200);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,7 @@ import { postManager } from '../stores/postManager';
|
|||||||
import { groupManager } from '../stores/groupManager';
|
import { groupManager } from '../stores/groupManager';
|
||||||
import { userManager } from '../stores/userManager';
|
import { userManager } from '../stores/userManager';
|
||||||
import { messageManager } from '../stores/messageManager';
|
import { messageManager } from '../stores/messageManager';
|
||||||
|
import type { PostListTab } from '../stores/postListSources';
|
||||||
|
|
||||||
// ==================== 预取配置 ====================
|
// ==================== 预取配置 ====================
|
||||||
|
|
||||||
@@ -134,7 +135,7 @@ const prefetchService = new PrefetchService();
|
|||||||
* 预取帖子数据
|
* 预取帖子数据
|
||||||
* @param types 帖子类型数组
|
* @param types 帖子类型数组
|
||||||
*/
|
*/
|
||||||
function prefetchPosts(types: string[] = ['latest', 'hot']): void {
|
function prefetchPosts(types: PostListTab[] = ['latest', 'hot']): void {
|
||||||
types.forEach((type, index) => {
|
types.forEach((type, index) => {
|
||||||
prefetchService.schedule({
|
prefetchService.schedule({
|
||||||
key: `posts:${type}:1`,
|
key: `posts:${type}:1`,
|
||||||
@@ -279,7 +280,7 @@ export function usePrefetch() {
|
|||||||
const hasInitialPrefetched = useRef(false);
|
const hasInitialPrefetched = useRef(false);
|
||||||
|
|
||||||
/** 预取帖子 */
|
/** 预取帖子 */
|
||||||
const prefetchPostsData = useCallback((types?: string[]) => {
|
const prefetchPostsData = useCallback((types?: PostListTab[]) => {
|
||||||
prefetchPosts(types);
|
prefetchPosts(types);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export const MaterialDetailScreen: React.FC = () => {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.centerContainer}>
|
<View style={styles.centerContainer}>
|
||||||
<Loading size="large" />
|
<Loading size="lg" />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -280,7 +280,7 @@ export const MaterialDetailScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.headerCenter}>
|
<View style={styles.headerCenter}>
|
||||||
<Text style={[styles.headerTitle, isWideScreen && styles.headerTitleWide]}>
|
<Text style={isWideScreen ? [styles.headerTitle, styles.headerTitleWide] : styles.headerTitle}>
|
||||||
资料详情
|
资料详情
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export const MaterialsScreen: React.FC = () => {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.centerContainer}>
|
<View style={styles.centerContainer}>
|
||||||
<Loading size="large" />
|
<Loading size="lg" />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -165,7 +165,7 @@ export const MaterialsScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.headerCenter}>
|
<View style={styles.headerCenter}>
|
||||||
<Text style={[styles.headerTitle, isWideScreen && styles.headerTitleWide]}>
|
<Text style={isWideScreen ? [styles.headerTitle, styles.headerTitleWide] : styles.headerTitle}>
|
||||||
学习资料
|
学习资料
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ export const SubjectMaterialsScreen: React.FC = () => {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.centerContainer}>
|
<View style={styles.centerContainer}>
|
||||||
<Loading size="large" />
|
<Loading size="lg" />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -254,7 +254,7 @@ export const SubjectMaterialsScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.headerCenter}>
|
<View style={styles.headerCenter}>
|
||||||
<Text style={[styles.headerTitle, isWideScreen && styles.headerTitleWide]}>
|
<Text style={isWideScreen ? [styles.headerTitle, styles.headerTitleWide] : styles.headerTitle}>
|
||||||
{subject?.name || '资料列表'}
|
{subject?.name || '资料列表'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -9,17 +9,10 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
|
|||||||
import { showConfirm } from './dialogService';
|
import { showConfirm } from './dialogService';
|
||||||
|
|
||||||
// 条件导入原生模块(仅在 Android 上可用)
|
// 条件导入原生模块(仅在 Android 上可用)
|
||||||
let FileSystem: typeof import('expo-file-system/legacy') | null = null;
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
let IntentLauncher: typeof import('expo-intent-launcher') | null = null;
|
const FileSystem = Platform.OS === 'android' ? require('expo-file-system/legacy') : null;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
if (Platform.OS === 'android') {
|
const IntentLauncher = Platform.OS === 'android' ? require('expo-intent-launcher') : null;
|
||||||
try {
|
|
||||||
FileSystem = require('expo-file-system/legacy');
|
|
||||||
IntentLauncher = require('expo-intent-launcher');
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to load native modules for APK update:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新服务器地址
|
// 更新服务器地址
|
||||||
const UPDATES_SERVER_URL = Constants.expoConfig?.extra?.updatesUrl
|
const UPDATES_SERVER_URL = Constants.expoConfig?.extra?.updatesUrl
|
||||||
|
|||||||
@@ -78,7 +78,13 @@ function normalizeAnnouncementCursor(
|
|||||||
// 群组服务类(纯 API 层)
|
// 群组服务类(纯 API 层)
|
||||||
// SQLite/内存缓存编排由 groupManager 统一处理
|
// SQLite/内存缓存编排由 groupManager 统一处理
|
||||||
class GroupService {
|
class GroupService {
|
||||||
async fetchGroupsFromApi(page = 1, pageSize = 20): Promise<GroupListResponse> {
|
// ==================== 群组管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取群组列表
|
||||||
|
* GET /api/v1/groups
|
||||||
|
*/
|
||||||
|
async getGroups(page = 1, pageSize = 20): Promise<GroupListResponse> {
|
||||||
const response = await api.get<GroupListResponse>('/groups', {
|
const response = await api.get<GroupListResponse>('/groups', {
|
||||||
page,
|
page,
|
||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
@@ -86,16 +92,20 @@ class GroupService {
|
|||||||
return normalizeGroupListResponse(response.data);
|
return normalizeGroupListResponse(response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchGroupFromApi(id: string): Promise<GroupResponse> {
|
/**
|
||||||
|
* 获取群组详情
|
||||||
|
* GET /api/v1/groups/:id
|
||||||
|
*/
|
||||||
|
async getGroup(id: string): Promise<GroupResponse> {
|
||||||
const response = await api.get<GroupResponse>(`/groups/${encodeURIComponent(id)}`);
|
const response = await api.get<GroupResponse>(`/groups/${encodeURIComponent(id)}`);
|
||||||
return normalizeGroup(response.data);
|
return normalizeGroup(response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchMembersFromApi(
|
/**
|
||||||
groupId: string,
|
* 获取群组成员列表
|
||||||
page = 1,
|
* GET /api/v1/groups/:id/members
|
||||||
pageSize = 50
|
*/
|
||||||
): Promise<GroupMemberListResponse> {
|
async getMembers(groupId: string, page = 1, pageSize = 50): Promise<GroupMemberListResponse> {
|
||||||
const response = await api.get<GroupMemberListResponse>(
|
const response = await api.get<GroupMemberListResponse>(
|
||||||
`/groups/${encodeURIComponent(groupId)}/members`,
|
`/groups/${encodeURIComponent(groupId)}/members`,
|
||||||
{
|
{
|
||||||
@@ -106,8 +116,6 @@ class GroupService {
|
|||||||
return normalizeMemberListResponse(response.data);
|
return normalizeMemberListResponse(response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 群组管理 ====================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建群组
|
* 创建群组
|
||||||
* POST /api/v1/groups
|
* POST /api/v1/groups
|
||||||
@@ -117,22 +125,6 @@ class GroupService {
|
|||||||
return normalizeGroup(response.data);
|
return normalizeGroup(response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取群组列表
|
|
||||||
* GET /api/v1/groups
|
|
||||||
*/
|
|
||||||
async getGroups(page = 1, pageSize = 20): Promise<GroupListResponse> {
|
|
||||||
return this.fetchGroupsFromApi(page, pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取群组详情
|
|
||||||
* GET /api/v1/groups/:id
|
|
||||||
*/
|
|
||||||
async getGroup(id: string): Promise<GroupResponse> {
|
|
||||||
return this.fetchGroupFromApi(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前用户在群组中的成员信息
|
* 获取当前用户在群组中的成员信息
|
||||||
* GET /api/v1/groups/:id/me
|
* GET /api/v1/groups/:id/me
|
||||||
@@ -211,14 +203,6 @@ class GroupService {
|
|||||||
await api.post(`/groups/${encodeURIComponent(groupId)}/leave`);
|
await api.post(`/groups/${encodeURIComponent(groupId)}/leave`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取群组成员列表
|
|
||||||
* GET /api/v1/groups/:id/members
|
|
||||||
*/
|
|
||||||
async getMembers(groupId: string, page = 1, pageSize = 50): Promise<GroupMemberListResponse> {
|
|
||||||
return this.fetchMembersFromApi(groupId, page, pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移除群成员(踢人)
|
* 移除群成员(踢人)
|
||||||
* POST /api/v1/groups/:id/members/kick
|
* POST /api/v1/groups/:id/members/kick
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
saveConversationCache,
|
saveConversationCache,
|
||||||
saveConversationsWithRelatedCache,
|
saveConversationsWithRelatedCache,
|
||||||
updateConversationCacheUnreadCount,
|
updateConversationCacheUnreadCount,
|
||||||
|
saveUsersCache,
|
||||||
} from './database';
|
} from './database';
|
||||||
|
|
||||||
/** 远端会话列表分页(offset / cursor)统一结果:拉取成功后均已执行本地缓存同步 */
|
/** 远端会话列表分页(offset / cursor)统一结果:拉取成功后均已执行本地缓存同步 */
|
||||||
|
|||||||
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 {
|
import {
|
||||||
GroupListResponse,
|
GroupListResponse,
|
||||||
GroupMemberListResponse,
|
GroupMemberListResponse,
|
||||||
@@ -14,7 +22,19 @@ import {
|
|||||||
saveGroupsCache,
|
saveGroupsCache,
|
||||||
saveUsersCache,
|
saveUsersCache,
|
||||||
} from '../services/database';
|
} 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 {
|
interface GroupManagerSnapshot {
|
||||||
groupIds: string[];
|
groupIds: string[];
|
||||||
@@ -28,20 +48,18 @@ type GroupManagerEvent =
|
|||||||
| CacheEvent<GroupManagerSnapshot>
|
| CacheEvent<GroupManagerSnapshot>
|
||||||
| CacheEvent<{ error: unknown; context: string }>;
|
| CacheEvent<{ error: unknown; context: string }>;
|
||||||
|
|
||||||
|
// ==================== 常量 ====================
|
||||||
|
|
||||||
const GROUP_TTL = 60 * 1000;
|
const GROUP_TTL = 60 * 1000;
|
||||||
const MEMBERS_TTL = 120 * 1000;
|
const MEMBERS_TTL = 120 * 1000;
|
||||||
|
|
||||||
interface CacheEntry<T> {
|
// ==================== Manager 实现 ====================
|
||||||
data: T;
|
|
||||||
timestamp: number;
|
|
||||||
ttl: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
class GroupManager extends CacheBus<GroupManagerSnapshot, GroupManagerEvent> {
|
class GroupManager extends CachedManager<GroupManagerSnapshot, GroupManagerEvent> {
|
||||||
private groupsListCache: CacheEntry<GroupResponse[]> | null = null;
|
/** 群组详情缓存 */
|
||||||
private groupDetailCache = new Map<string, CacheEntry<GroupResponse | null>>();
|
private groupDetailCache = new Map<string, CacheEntry<GroupResponse | null>>();
|
||||||
|
/** 群组成员缓存 */
|
||||||
private groupMembersCache = new Map<string, CacheEntry<GroupMemberResponse[]>>();
|
private groupMembersCache = new Map<string, CacheEntry<GroupMemberResponse[]>>();
|
||||||
private pendingRequests = new Map<string, Promise<any>>();
|
|
||||||
|
|
||||||
protected getSnapshot(): GroupManagerSnapshot {
|
protected getSnapshot(): GroupManagerSnapshot {
|
||||||
return {
|
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(() => {
|
async getGroups(page = 1, pageSize = GROUP_LIST_PAGE_SIZE, forceRefresh = false): Promise<GroupListResponse> {
|
||||||
this.pendingRequests.delete(key);
|
const key = `list:${page}:${pageSize}`;
|
||||||
});
|
|
||||||
this.pendingRequests.set(key, request);
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getGroups(page = 1, pageSize = 20, forceRefresh = false): Promise<GroupListResponse> {
|
// 第一页且有有效缓存
|
||||||
if (page === 1 && !forceRefresh && this.groupsListCache && !this.isExpired(this.groupsListCache)) {
|
if (page === 1 && !forceRefresh && this.hasValidCache(key)) {
|
||||||
return {
|
const groups = this.getFromCache<GroupResponse[]>(key)!;
|
||||||
list: this.groupsListCache.data,
|
return this.buildGroupListResponse(groups, pageSize);
|
||||||
total: this.groupsListCache.data.length,
|
|
||||||
page: 1,
|
|
||||||
page_size: pageSize,
|
|
||||||
total_pages: 1,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page === 1 && !forceRefresh && this.groupsListCache && this.isExpired(this.groupsListCache)) {
|
// 第一页且缓存过期:后台刷新
|
||||||
|
if (page === 1 && !forceRefresh && this.hasExpiredCache(key)) {
|
||||||
this.refreshGroupsInBackground(page, pageSize);
|
this.refreshGroupsInBackground(page, pageSize);
|
||||||
return {
|
const groups = this.getFromCache<GroupResponse[]>(key)!;
|
||||||
list: this.groupsListCache.data,
|
return this.buildGroupListResponse(groups, pageSize);
|
||||||
total: this.groupsListCache.data.length,
|
|
||||||
page: 1,
|
|
||||||
page_size: pageSize,
|
|
||||||
total_pages: 1,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 第一页:尝试本地缓存
|
||||||
if (page === 1 && !forceRefresh) {
|
if (page === 1 && !forceRefresh) {
|
||||||
const localGroups = await getAllGroupsCache();
|
const localGroups = await getAllGroupsCache();
|
||||||
if (localGroups.length > 0) {
|
if (localGroups.length > 0) {
|
||||||
this.groupsListCache = { data: localGroups, timestamp: Date.now(), ttl: GROUP_TTL };
|
this.setCache(key, localGroups, GROUP_TTL);
|
||||||
this.notify({
|
this.notify({
|
||||||
type: 'list_updated',
|
type: 'list_updated',
|
||||||
payload: { groups: localGroups },
|
payload: { groups: localGroups },
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
this.refreshGroupsInBackground(page, pageSize);
|
this.refreshGroupsInBackground(page, pageSize);
|
||||||
return {
|
return this.buildGroupListResponse(localGroups, pageSize);
|
||||||
list: localGroups,
|
|
||||||
total: localGroups.length,
|
|
||||||
page: 1,
|
|
||||||
page_size: pageSize,
|
|
||||||
total_pages: 1,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 发起请求
|
||||||
return this.dedupe(`groups:list:${page}:${pageSize}`, async () => {
|
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 || [];
|
const groups = response.list || [];
|
||||||
if (page === 1) {
|
if (page === 1) {
|
||||||
this.groupsListCache = { data: groups, timestamp: Date.now(), ttl: GROUP_TTL };
|
this.setCache(key, groups, GROUP_TTL);
|
||||||
}
|
}
|
||||||
await saveGroupsCache(groups).catch(() => {});
|
await saveGroupsCache(groups).catch(() => {});
|
||||||
this.notify({
|
this.notify({
|
||||||
@@ -122,12 +121,23 @@ class GroupManager extends CacheBus<GroupManagerSnapshot, GroupManagerEvent> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private refreshGroupsInBackground(page = 1, pageSize = 20): void {
|
private buildGroupListResponse(groups: GroupResponse[], pageSize: number): GroupListResponse {
|
||||||
this.dedupe(`groups:list:bg:${page}:${pageSize}`, async () => {
|
return {
|
||||||
const response = await groupService.fetchGroupsFromApi(page, pageSize);
|
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 || [];
|
const groups = response.list || [];
|
||||||
if (page === 1) {
|
if (page === 1) {
|
||||||
this.groupsListCache = { data: groups, timestamp: Date.now(), ttl: GROUP_TTL };
|
this.setCache(key, groups, GROUP_TTL);
|
||||||
saveGroupsCache(groups).catch(() => {});
|
saveGroupsCache(groups).catch(() => {});
|
||||||
}
|
}
|
||||||
this.notify({
|
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> {
|
async getGroup(groupId: string, forceRefresh = false): Promise<GroupResponse> {
|
||||||
const id = groupId;
|
const cached = this.groupDetailCache.get(groupId);
|
||||||
const cached = this.groupDetailCache.get(id);
|
|
||||||
if (!forceRefresh && cached && !this.isExpired(cached) && cached.data) {
|
// 有效缓存
|
||||||
|
if (!forceRefresh && cached && !isCacheExpired(cached) && cached.data) {
|
||||||
return 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;
|
return cached.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 尝试本地缓存
|
||||||
if (!forceRefresh) {
|
if (!forceRefresh) {
|
||||||
const localGroup = await getGroupCache(id);
|
const localGroup = await getGroupCache(groupId);
|
||||||
if (localGroup) {
|
if (localGroup) {
|
||||||
this.groupDetailCache.set(id, { data: localGroup, timestamp: Date.now(), ttl: GROUP_TTL });
|
this.groupDetailCache.set(groupId, createCacheEntry(localGroup, GROUP_TTL));
|
||||||
this.notify({
|
this.notify({
|
||||||
type: 'detail_updated',
|
type: 'detail_updated',
|
||||||
payload: { groupId: id, group: localGroup },
|
payload: { groupId, group: localGroup },
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
this.refreshGroupInBackground(id);
|
this.refreshGroupInBackground(groupId);
|
||||||
return localGroup;
|
return localGroup;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.dedupe(`groups:detail:${id}`, async () => {
|
// 发起请求
|
||||||
const group = await groupService.fetchGroupFromApi(id);
|
return this.dedupe(`groups:detail:${groupId}`, async () => {
|
||||||
this.groupDetailCache.set(id, { data: group, timestamp: Date.now(), ttl: GROUP_TTL });
|
const group = await groupService.getGroup(groupId);
|
||||||
|
this.groupDetailCache.set(groupId, createCacheEntry(group, GROUP_TTL));
|
||||||
await saveGroupCache(group).catch(() => {});
|
await saveGroupCache(group).catch(() => {});
|
||||||
this.notify({
|
this.notify({
|
||||||
type: 'detail_updated',
|
type: 'detail_updated',
|
||||||
payload: { groupId: id, group },
|
payload: { groupId, group },
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
return group;
|
return group;
|
||||||
@@ -186,8 +215,8 @@ class GroupManager extends CacheBus<GroupManagerSnapshot, GroupManagerEvent> {
|
|||||||
|
|
||||||
private refreshGroupInBackground(groupId: string): void {
|
private refreshGroupInBackground(groupId: string): void {
|
||||||
this.dedupe(`groups:detail:bg:${groupId}`, async () => {
|
this.dedupe(`groups:detail:bg:${groupId}`, async () => {
|
||||||
const group = await groupService.fetchGroupFromApi(groupId);
|
const group = await groupService.getGroup(groupId);
|
||||||
this.groupDetailCache.set(groupId, { data: group, timestamp: Date.now(), ttl: GROUP_TTL });
|
this.groupDetailCache.set(groupId, createCacheEntry(group, GROUP_TTL));
|
||||||
saveGroupCache(group).catch(() => {});
|
saveGroupCache(group).catch(() => {});
|
||||||
this.notify({
|
this.notify({
|
||||||
type: 'detail_updated',
|
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);
|
const cached = this.groupMembersCache.get(key);
|
||||||
if (!forceRefresh && cached && !this.isExpired(cached)) {
|
|
||||||
return {
|
// 有效缓存
|
||||||
list: cached.data,
|
if (!forceRefresh && cached && !isCacheExpired(cached)) {
|
||||||
total: cached.data.length,
|
return this.buildMemberListResponse(cached.data, page, pageSize);
|
||||||
page,
|
|
||||||
page_size: pageSize,
|
|
||||||
total_pages: 1,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!forceRefresh && cached && this.isExpired(cached)) {
|
// 过期缓存:后台刷新
|
||||||
this.refreshMembersInBackground(id, page, pageSize);
|
if (!forceRefresh && cached && isCacheExpired(cached)) {
|
||||||
return {
|
this.refreshMembersInBackground(groupId, page, pageSize);
|
||||||
list: cached.data,
|
return this.buildMemberListResponse(cached.data, page, pageSize);
|
||||||
total: cached.data.length,
|
|
||||||
page,
|
|
||||||
page_size: pageSize,
|
|
||||||
total_pages: 1,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 第一页:尝试本地缓存
|
||||||
if (page === 1 && !forceRefresh) {
|
if (page === 1 && !forceRefresh) {
|
||||||
const localMembers = await getGroupMembersCache(id);
|
const localMembers = await getGroupMembersCache(groupId);
|
||||||
if (localMembers.length > 0) {
|
if (localMembers.length > 0) {
|
||||||
this.groupMembersCache.set(key, {
|
this.groupMembersCache.set(key, createCacheEntry(localMembers, MEMBERS_TTL));
|
||||||
data: localMembers,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
ttl: MEMBERS_TTL,
|
|
||||||
});
|
|
||||||
this.notify({
|
this.notify({
|
||||||
type: 'detail_updated',
|
type: 'detail_updated',
|
||||||
payload: { groupId: id, members: localMembers },
|
payload: { groupId, members: localMembers },
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
this.refreshMembersInBackground(id, page, pageSize);
|
this.refreshMembersInBackground(groupId, page, pageSize);
|
||||||
return {
|
return this.buildMemberListResponse(localMembers, page, pageSize);
|
||||||
list: localMembers,
|
|
||||||
total: localMembers.length,
|
|
||||||
page,
|
|
||||||
page_size: pageSize,
|
|
||||||
total_pages: 1,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 发起请求
|
||||||
return this.dedupe(`groups:members:${key}`, async () => {
|
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 || [];
|
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) {
|
if (page === 1) {
|
||||||
await saveGroupMembersCache(id, members).catch(() => {});
|
await saveGroupMembersCache(groupId, members).catch(() => {});
|
||||||
}
|
}
|
||||||
await saveUsersCache(
|
await saveUsersCache(
|
||||||
members
|
members
|
||||||
@@ -267,19 +288,37 @@ class GroupManager extends CacheBus<GroupManagerSnapshot, GroupManagerEvent> {
|
|||||||
).catch(() => {});
|
).catch(() => {});
|
||||||
this.notify({
|
this.notify({
|
||||||
type: 'detail_updated',
|
type: 'detail_updated',
|
||||||
payload: { groupId: id, members },
|
payload: { groupId, members },
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private refreshMembersInBackground(groupId: string, page = 1, pageSize = 50): void {
|
private buildMemberListResponse(
|
||||||
this.dedupe(`groups:members:bg:${groupId}:${page}:${pageSize}`, async () => {
|
members: GroupMemberResponse[],
|
||||||
const response = await groupService.fetchMembersFromApi(groupId, page, pageSize);
|
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 members = response.list || [];
|
||||||
const key = `${groupId}:${page}:${pageSize}`;
|
this.groupMembersCache.set(key, createCacheEntry(members, MEMBERS_TTL));
|
||||||
this.groupMembersCache.set(key, { data: members, timestamp: Date.now(), ttl: MEMBERS_TTL });
|
|
||||||
if (page === 1) {
|
if (page === 1) {
|
||||||
saveGroupMembersCache(groupId, members).catch(() => {});
|
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 {
|
invalidate(groupId?: string): void {
|
||||||
if (!groupId) {
|
if (!groupId) {
|
||||||
this.groupsListCache = null;
|
this.clearCache();
|
||||||
this.groupDetailCache.clear();
|
this.groupDetailCache.clear();
|
||||||
this.groupMembersCache.clear();
|
this.groupMembersCache.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.groupDetailCache.delete(groupId);
|
this.groupDetailCache.delete(groupId);
|
||||||
[...this.groupMembersCache.keys()].forEach((key) => {
|
[...this.groupMembersCache.keys()].forEach((key) => {
|
||||||
if (key.startsWith(`${groupId}:`)) {
|
if (key.startsWith(`members:${groupId}:`)) {
|
||||||
this.groupMembersCache.delete(key);
|
this.groupMembersCache.delete(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有数据
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
this.clearCache();
|
||||||
|
this.groupDetailCache.clear();
|
||||||
|
this.groupMembersCache.clear();
|
||||||
|
this.clearPendingRequests();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const groupManager = new GroupManager();
|
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 { Post } from '../types';
|
||||||
import { postService } from '../services/postService';
|
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 {
|
interface PostListPayload {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -23,66 +41,57 @@ type PostManagerEvent =
|
|||||||
| CacheEvent<PostManagerSnapshot>
|
| CacheEvent<PostManagerSnapshot>
|
||||||
| CacheEvent<{ error: unknown; context: string }>;
|
| CacheEvent<{ error: unknown; context: string }>;
|
||||||
|
|
||||||
|
// ==================== 常量 ====================
|
||||||
|
|
||||||
const LIST_TTL = 30 * 1000;
|
const LIST_TTL = 30 * 1000;
|
||||||
const DETAIL_TTL = 60 * 1000;
|
const DETAIL_TTL = 60 * 1000;
|
||||||
|
|
||||||
interface CacheEntry<T> {
|
// ==================== Manager 实现 ====================
|
||||||
data: T;
|
|
||||||
timestamp: number;
|
|
||||||
ttl: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
class PostManager extends CacheBus<PostManagerSnapshot, PostManagerEvent> {
|
class PostManager extends CachedManager<PostManagerSnapshot, PostManagerEvent> {
|
||||||
private listCache = new Map<string, CacheEntry<Post[]>>();
|
/** 详情缓存 */
|
||||||
private detailCache = new Map<string, CacheEntry<Post | null>>();
|
private detailCache = new Map<string, CacheEntry<Post | null>>();
|
||||||
private pendingRequests = new Map<string, Promise<any>>();
|
|
||||||
|
|
||||||
protected getSnapshot(): PostManagerSnapshot {
|
protected getSnapshot(): PostManagerSnapshot {
|
||||||
return {
|
return {
|
||||||
listKeys: [...this.listCache.keys()],
|
listKeys: [...this.memoryCache.keys()],
|
||||||
detailKeys: [...this.detailCache.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 {
|
private listKey(type: string, page: number, pageSize: number): string {
|
||||||
return `${type}:${page}:${pageSize}`;
|
return `list:${type}:${page}:${pageSize}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取帖子列表(传统分页模式)
|
||||||
|
*/
|
||||||
async getPosts(
|
async getPosts(
|
||||||
type = 'hot',
|
type: PostListTab = 'hot',
|
||||||
page = 1,
|
page = 1,
|
||||||
pageSize = 20,
|
pageSize = POST_LIST_PAGE_SIZE,
|
||||||
forceRefresh = false
|
forceRefresh = false
|
||||||
): Promise<Post[]> {
|
): Promise<Post[]> {
|
||||||
const key = this.listKey(type, page, pageSize);
|
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);
|
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 response = await postService.getPosts(page, pageSize, type);
|
||||||
const posts = response.list || [];
|
const posts = response.list || [];
|
||||||
this.listCache.set(key, { data: posts, timestamp: Date.now(), ttl: LIST_TTL });
|
this.setCache(key, posts, LIST_TTL);
|
||||||
this.notify({
|
this.notify({
|
||||||
type: 'list_updated',
|
type: 'list_updated',
|
||||||
payload: { key, posts },
|
payload: { key, posts },
|
||||||
@@ -92,11 +101,16 @@ class PostManager extends CacheBus<PostManagerSnapshot, PostManagerEvent> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private refreshPostsInBackground(key: string, type: string, page: number, pageSize: number): void {
|
private refreshPostsInBackground(
|
||||||
this.dedupe(`posts:list:bg:${key}`, async () => {
|
key: string,
|
||||||
|
type: PostListTab,
|
||||||
|
page: number,
|
||||||
|
pageSize: number
|
||||||
|
): void {
|
||||||
|
this.dedupe(`posts:bg:${key}`, async () => {
|
||||||
const response = await postService.getPosts(page, pageSize, type);
|
const response = await postService.getPosts(page, pageSize, type);
|
||||||
const posts = response.list || [];
|
const posts = response.list || [];
|
||||||
this.listCache.set(key, { data: posts, timestamp: Date.now(), ttl: LIST_TTL });
|
this.setCache(key, posts, LIST_TTL);
|
||||||
this.notify({
|
this.notify({
|
||||||
type: 'list_updated',
|
type: 'list_updated',
|
||||||
payload: { key, posts },
|
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> {
|
async getPostDetail(postId: string, forceRefresh = false): Promise<Post | null> {
|
||||||
const cached = this.detailCache.get(postId);
|
const cached = this.detailCache.get(postId);
|
||||||
if (!forceRefresh && cached && !this.isExpired(cached)) {
|
|
||||||
|
// 有效缓存
|
||||||
|
if (!forceRefresh && cached && !isCacheExpired(cached)) {
|
||||||
return cached.data;
|
return cached.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!forceRefresh && cached && this.isExpired(cached)) {
|
// 过期缓存:后台刷新
|
||||||
|
if (!forceRefresh && cached && isCacheExpired(cached) && cached.data) {
|
||||||
this.refreshPostDetailInBackground(postId);
|
this.refreshPostDetailInBackground(postId);
|
||||||
return cached.data;
|
return cached.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 无缓存:发起请求
|
||||||
return this.dedupe(`posts:detail:${postId}`, async () => {
|
return this.dedupe(`posts:detail:${postId}`, async () => {
|
||||||
const post = await postService.getPost(postId);
|
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({
|
this.notify({
|
||||||
type: 'detail_updated',
|
type: 'detail_updated',
|
||||||
payload: { postId, post },
|
payload: { postId, post },
|
||||||
@@ -138,7 +171,7 @@ class PostManager extends CacheBus<PostManagerSnapshot, PostManagerEvent> {
|
|||||||
private refreshPostDetailInBackground(postId: string): void {
|
private refreshPostDetailInBackground(postId: string): void {
|
||||||
this.dedupe(`posts:detail:bg:${postId}`, async () => {
|
this.dedupe(`posts:detail:bg:${postId}`, async () => {
|
||||||
const post = await postService.getPost(postId);
|
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({
|
this.notify({
|
||||||
type: 'detail_updated',
|
type: 'detail_updated',
|
||||||
payload: { postId, post },
|
payload: { postId, post },
|
||||||
@@ -154,21 +187,28 @@ class PostManager extends CacheBus<PostManagerSnapshot, PostManagerEvent> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 缓存管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使缓存失效
|
||||||
|
*/
|
||||||
invalidate(postId?: string): void {
|
invalidate(postId?: string): void {
|
||||||
if (postId) {
|
if (postId) {
|
||||||
this.detailCache.delete(postId);
|
this.detailCache.delete(postId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.listCache.clear();
|
this.clearCache();
|
||||||
this.detailCache.clear();
|
this.detailCache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有数据
|
||||||
|
*/
|
||||||
clear(): void {
|
clear(): void {
|
||||||
this.listCache.clear();
|
this.clearCache();
|
||||||
this.detailCache.clear();
|
this.detailCache.clear();
|
||||||
this.pendingRequests.clear();
|
this.clearPendingRequests();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const postManager = new PostManager();
|
export const postManager = new PostManager();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user