feat(infrastructure): add pagination, cache, and sync infrastructure
All checks were successful
Frontend CI / ota-android (push) Successful in 14m38s
Frontend CI / build-android-apk (push) Successful in 1h0m2s
Frontend CI / build-and-push-web (push) Successful in 10m29s

- Add pagination infrastructure with usePagination and usePaginationManager hooks
- Add connection state management hook for network status tracking
- Add media caching infrastructure with useMediaCache hook
- Add differential message synchronization infrastructure
- Export new hooks from central index file
- Remove backup file MainNavigator.tsx.bak
This commit is contained in:
lafay
2026-03-18 23:05:28 +08:00
parent a9c514f664
commit 62b55aec31
20 changed files with 5893 additions and 1126 deletions

1725
docs/OPTIMIZATION_DESIGN.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -71,3 +71,24 @@ export {
prefetchMessageScreen,
prefetchHomeScreen,
} from './usePrefetch';
// ==================== 分页 Hooks ====================
export {
usePagination,
usePaginationManager,
} from './usePagination';
export type {
UsePaginationOptions,
UsePaginationReturn,
} from './usePagination';
// ==================== 连接状态 Hooks ====================
export { useConnectionState } from './useConnectionState';
export type { UseConnectionStateResult } from './useConnectionState';
// ==================== 媒体缓存 Hooks ====================
export { useMediaCache } from './useMediaCache';
export type { UseMediaCacheReturn } from './useMediaCache';

View File

@@ -0,0 +1,147 @@
/**
* 连接状态 Hook
*
* 提供连接状态的 React Hook 接口
* 订阅 ConnectionStateManager 状态变化
*/
import { useState, useEffect, useCallback, useMemo } from 'react';
import {
ConnectionState,
ConnectionStateInfo,
connectionStateManager,
} from '../infrastructure/sync';
/**
* useConnectionState Hook 返回值接口
*/
export interface UseConnectionStateResult {
/** 当前连接状态 */
state: ConnectionState;
/** 完整的状态信息 */
stateInfo: ConnectionStateInfo;
/** 是否已连接 */
isConnected: boolean;
/** 是否正在连接(包括 CONNECTING 和 RECONNECTING */
isConnecting: boolean;
/** 是否正在重连 */
isReconnecting: boolean;
/** 是否已断开 */
isDisconnected: boolean;
/** 是否处于错误状态 */
hasError: boolean;
/** 当前错误信息 */
error: Error | null;
/** 当前重连次数 */
reconnectAttempts: number;
/** 最大重连次数 */
maxReconnectAttempts: number;
/** 最后连接时间 */
lastConnectedAt: number | null;
/** 状态描述文本 */
description: string;
/** 手动触发重连 */
triggerReconnect: () => void;
/** 重置重连次数 */
resetReconnectAttempts: () => void;
}
/**
* 连接状态 Hook
*
* 订阅 ConnectionStateManager 的状态变化,
* 提供便捷的状态属性和方法
*
* @example
* ```tsx
* function ConnectionStatus() {
* const { state, isConnected, isConnecting, description } = useConnectionState();
*
* return (
* <View>
* <Text>{description}</Text>
* {isConnecting && <ActivityIndicator />}
* </View>
* );
* }
* ```
*/
export function useConnectionState(): UseConnectionStateResult {
// 状态
const [stateInfo, setStateInfo] = useState<ConnectionStateInfo>(() =>
connectionStateManager.getState()
);
// 订阅状态变化
useEffect(() => {
const unsubscribe = connectionStateManager.subscribe((newState) => {
setStateInfo(newState);
});
return unsubscribe;
}, []);
// 计算派生状态
const isConnected = useMemo(() =>
stateInfo.state === ConnectionState.CONNECTED,
[stateInfo.state]
);
const isConnecting = useMemo(() =>
stateInfo.state === ConnectionState.CONNECTING ||
stateInfo.state === ConnectionState.RECONNECTING,
[stateInfo.state]
);
const isReconnecting = useMemo(() =>
stateInfo.state === ConnectionState.RECONNECTING,
[stateInfo.state]
);
const isDisconnected = useMemo(() =>
stateInfo.state === ConnectionState.DISCONNECTED ||
stateInfo.state === ConnectionState.IDLE,
[stateInfo.state]
);
const hasError = useMemo(() =>
stateInfo.state === ConnectionState.ERROR,
[stateInfo.state]
);
// 手动触发重连
const triggerReconnect = useCallback(() => {
// 只有在断开或错误状态才能触发重连
if (
stateInfo.state === ConnectionState.DISCONNECTED ||
stateInfo.state === ConnectionState.ERROR ||
stateInfo.state === ConnectionState.IDLE
) {
connectionStateManager.setState(ConnectionState.RECONNECTING);
}
}, [stateInfo.state]);
// 重置重连次数
const resetReconnectAttempts = useCallback(() => {
connectionStateManager.resetReconnectAttempts();
}, []);
return {
state: stateInfo.state,
stateInfo,
isConnected,
isConnecting,
isReconnecting,
isDisconnected,
hasError,
error: stateInfo.error || null,
reconnectAttempts: stateInfo.reconnectAttempts || 0,
maxReconnectAttempts: connectionStateManager.getMaxReconnectAttempts(),
lastConnectedAt: stateInfo.lastConnectedAt || null,
description: stateInfo.description,
triggerReconnect,
resetReconnectAttempts,
};
}
export default useConnectionState;

View File

@@ -0,0 +1,369 @@
/**
* 差异更新 Hook
* 接收原始消息列表,返回优化后的消息列表,自动处理批量更新
*/
import { useState, useRef, useCallback, useEffect } from 'react';
import {
MessageIdentifier,
MessageUpdate,
MessageUpdateType,
DiffConfig,
} from '../infrastructure/diff/types';
import {
MessageDiffCalculator,
createMessageDiffCalculator,
} from '../infrastructure/diff/MessageDiffCalculator';
import {
MessageUpdateBatcher,
createMessageUpdateBatcher,
BatcherOptions,
} from '../infrastructure/diff/MessageUpdateBatcher';
/**
* 差异更新 Hook 配置选项
*/
export interface UseDifferentialMessagesOptions<T extends MessageIdentifier> {
/** 差异计算配置 */
diffConfig?: DiffConfig;
/** 批量处理配置 */
batcherOptions?: BatcherOptions;
/** 是否启用差异计算,默认 true */
enableDiff?: boolean;
/** 是否启用批量处理,默认 true */
enableBatching?: boolean;
/** 最大消息数量限制,超出时触发重置 */
maxMessageCount?: number;
/** 变化比例阈值0-1超过此值触发重置 */
changeRatioThreshold?: number;
}
/**
* 差异更新 Hook 返回值
*/
export interface UseDifferentialMessagesResult<T extends MessageIdentifier> {
/** 优化后的消息列表 */
optimizedMessages: T[];
/** 待处理更新数量 */
pendingUpdateCount: number;
/** 是否正在处理更新 */
isProcessing: boolean;
/** 手动刷新方法 */
flush: () => void;
/** 重置方法 */
reset: () => void;
/** 强制刷新(跳过批量处理) */
forceUpdate: (messages: T[]) => void;
/** 获取差异统计信息 */
getDiffStats: () => {
totalBatches: number;
totalUpdates: number;
averageBatchSize: number;
};
}
/**
* 差异更新 Hook
* @param messages 原始消息列表
* @param options 配置选项
* @returns 优化后的消息列表和相关方法
*/
export function useDifferentialMessages<T extends MessageIdentifier>(
messages: T[],
options: UseDifferentialMessagesOptions<T> = {}
): UseDifferentialMessagesResult<T> {
const {
diffConfig,
batcherOptions,
enableDiff = true,
enableBatching = true,
maxMessageCount = 10000,
changeRatioThreshold = 0.5,
} = options;
// 状态
const [optimizedMessages, setOptimizedMessages] = useState<T[]>(messages);
const [isProcessing, setIsProcessing] = useState(false);
const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
// Refs
const calculatorRef = useRef<MessageDiffCalculator<T> | null>(null);
const batcherRef = useRef<MessageUpdateBatcher | null>(null);
const previousMessagesRef = useRef<T[]>([]);
// 初始化差异计算器
useEffect(() => {
if (enableDiff) {
calculatorRef.current = createMessageDiffCalculator<T>(diffConfig);
}
return () => {
calculatorRef.current = null;
};
}, [enableDiff, diffConfig]);
// 初始化批量处理器
useEffect(() => {
if (enableBatching) {
batcherRef.current = createMessageUpdateBatcher(batcherOptions);
// 订阅批量更新
const unsubscribe = batcherRef.current.subscribe((updates) => {
handleBatchUpdates(updates);
});
return () => {
unsubscribe();
batcherRef.current?.destroy();
batcherRef.current = null;
};
}
}, [enableBatching, batcherOptions]);
// 处理批量更新
const handleBatchUpdates = useCallback((updates: MessageUpdate[]) => {
setIsProcessing(true);
try {
setOptimizedMessages((currentMessages) => {
let newMessages = [...currentMessages];
for (const update of updates) {
switch (update.type) {
case MessageUpdateType.ADD: {
const addUpdate = update as any;
const index = addUpdate.index ?? newMessages.length;
newMessages.splice(index, 0, addUpdate.message);
break;
}
case MessageUpdateType.BATCH_ADD: {
const batchAddUpdate = update as any;
const messages = batchAddUpdate.messages || [];
const startIndex = batchAddUpdate.startIndex ?? newMessages.length;
newMessages.splice(startIndex, 0, ...messages);
break;
}
case MessageUpdateType.UPDATE: {
const updateMsg = update as any;
const index = newMessages.findIndex(m => m.id === updateMsg.messageId);
if (index !== -1) {
newMessages[index] = { ...newMessages[index], ...updateMsg.updates };
}
break;
}
case MessageUpdateType.BATCH_UPDATE: {
const batchUpdate = update as any;
const updates = batchUpdate.updates || [];
for (const { messageId, changes } of updates) {
const index = newMessages.findIndex(m => m.id === messageId);
if (index !== -1) {
newMessages[index] = { ...newMessages[index], ...changes };
}
}
break;
}
case MessageUpdateType.DELETE: {
const deleteUpdate = update as any;
newMessages = newMessages.filter(m => m.id !== deleteUpdate.messageId);
break;
}
case MessageUpdateType.BATCH_DELETE: {
const batchDelete = update as any;
const idsToDelete = new Set(batchDelete.messageIds || []);
newMessages = newMessages.filter(m => !idsToDelete.has(m.id));
break;
}
case MessageUpdateType.MOVE: {
const moveUpdate = update as any;
const fromIndex = newMessages.findIndex(m => m.id === moveUpdate.messageId);
if (fromIndex !== -1) {
const [movedMessage] = newMessages.splice(fromIndex, 1);
newMessages.splice(moveUpdate.toIndex, 0, movedMessage);
}
break;
}
case MessageUpdateType.RESET: {
const resetUpdate = update as any;
newMessages = resetUpdate.messages || [];
break;
}
}
}
return newMessages;
});
} finally {
setIsProcessing(false);
setPendingUpdateCount(batcherRef.current?.getPendingCount() ?? 0);
}
}, []);
// 监听外部消息变化
useEffect(() => {
// 如果消息数量变化太大,直接重置
const previousCount = previousMessagesRef.current.length;
const currentCount = messages.length;
const changeRatio = previousCount === 0 ? 1 : Math.abs(currentCount - previousCount) / previousCount;
if (changeRatio > changeRatioThreshold) {
setOptimizedMessages(messages);
previousMessagesRef.current = messages;
return;
}
// 使用差异计算
if (enableDiff && calculatorRef.current) {
const diff = calculatorRef.current.calculateDiff(previousMessagesRef.current, messages);
if (diff.shouldReset) {
setOptimizedMessages(messages);
} else {
// 将差异转换为更新操作
const updates: MessageUpdate[] = [];
// 处理新增
if (diff.added.length > 0) {
if (diff.added.length === 1) {
updates.push({
type: MessageUpdateType.ADD,
timestamp: Date.now(),
message: diff.added[0],
} as any);
} else {
updates.push({
type: MessageUpdateType.BATCH_ADD,
timestamp: Date.now(),
messages: diff.added,
} as any);
}
}
// 处理更新
if (diff.updated.length > 0) {
if (diff.updated.length === 1) {
updates.push({
type: MessageUpdateType.UPDATE,
timestamp: Date.now(),
messageId: diff.updated[0].message.id,
updates: diff.updated[0].changes,
} as any);
} else {
updates.push({
type: MessageUpdateType.BATCH_UPDATE,
timestamp: Date.now(),
updates: diff.updated.map(u => ({
messageId: u.message.id,
changes: u.changes,
})),
} as any);
}
}
// 处理删除
if (diff.deleted.length > 0) {
if (diff.deleted.length === 1) {
updates.push({
type: MessageUpdateType.DELETE,
timestamp: Date.now(),
messageId: diff.deleted[0].message.id,
} as any);
} else {
updates.push({
type: MessageUpdateType.BATCH_DELETE,
timestamp: Date.now(),
messageIds: diff.deleted.map(d => d.message.id),
} as any);
}
}
// 处理移动
if (diff.moved.length > 0) {
for (const move of diff.moved) {
updates.push({
type: MessageUpdateType.MOVE,
timestamp: Date.now(),
messageId: move.message.id,
toIndex: move.toIndex,
} as any);
}
}
// 应用更新
if (updates.length > 0) {
if (enableBatching && batcherRef.current) {
batcherRef.current.addUpdates(updates);
} else {
handleBatchUpdates(updates);
}
}
}
} else {
// 不使用差异计算,直接设置
setOptimizedMessages(messages);
}
// 更新消息数量限制检查
if (messages.length > maxMessageCount) {
console.warn(`[useDifferentialMessages] Message count (${messages.length}) exceeds limit (${maxMessageCount})`);
}
previousMessagesRef.current = messages;
}, [messages, enableDiff, enableBatching, maxMessageCount, changeRatioThreshold]);
// 手动刷新方法
const flush = useCallback(() => {
batcherRef.current?.flush();
}, []);
// 重置方法
const reset = useCallback(() => {
calculatorRef.current?.reset();
batcherRef.current?.clearPending();
setOptimizedMessages([]);
previousMessagesRef.current = [];
}, []);
// 强制更新方法
const forceUpdate = useCallback((newMessages: T[]) => {
setOptimizedMessages(newMessages);
}, []);
// 获取统计信息
const getDiffStats = useCallback(() => {
const batcherStats = batcherRef.current?.getStats();
return {
totalBatches: batcherStats?.totalBatches ?? 0,
totalUpdates: batcherStats?.totalUpdates ?? 0,
averageBatchSize: batcherStats?.averageBatchSize ?? 0,
};
}, []);
// 定期更新待处理数量
useEffect(() => {
if (!enableBatching || !batcherRef.current) return;
const interval = setInterval(() => {
setPendingUpdateCount(batcherRef.current?.getPendingCount() ?? 0);
}, 50);
return () => clearInterval(interval);
}, [enableBatching]);
return {
optimizedMessages,
pendingUpdateCount,
isProcessing,
flush,
reset,
forceUpdate,
getDiffStats,
};
}
export default useDifferentialMessages;

241
src/hooks/useMediaCache.ts Normal file
View File

@@ -0,0 +1,241 @@
/**
* 媒体缓存 Hook
* @module src/hooks/useMediaCache
* @description 提供媒体缓存管理的 React Hook
*/
import { useState, useCallback, useEffect, useRef } from 'react';
import { mediaCacheManager } from '../infrastructure/cache';
import {
CacheStats,
CleanupPolicy,
CleanupResult,
MediaType,
CleanupTrigger,
DEFAULT_CLEANUP_POLICY,
} from '../infrastructure/cache/types';
/**
* useMediaCache Hook 返回值类型
*/
export interface UseMediaCacheReturn {
/** 缓存统计信息 */
stats: CacheStats | null;
/** 是否正在清理 */
isClearing: boolean;
/** 是否已初始化 */
isInitialized: boolean;
/** 加载缓存统计 */
loadStats: () => void;
/** 执行清理(手动) */
cleanup: (policy?: Partial<CleanupPolicy>) => Promise<CleanupResult>;
/** 清空所有缓存 */
clearAll: () => Promise<void>;
/** 清理指定会话的缓存 */
clearConversation: (conversationId: string) => Promise<CleanupResult>;
/** 启动定期清理 */
startPeriodicCleanup: () => void;
/** 停止定期清理 */
stopPeriodicCleanup: () => void;
/** 记录缓存访问(用于 LRU */
recordAccess: (key: string) => Promise<void>;
/** 格式化缓存大小 */
formatSize: (bytes: number) => string;
/** 缓存媒体文件 */
cacheMedia: (
uri: string,
type: MediaType,
options?: {
conversationId?: string;
messageId?: string;
size?: number;
}
) => Promise<string>;
/** 获取缓存的媒体路径 */
getCachedMedia: (uri: string, type: MediaType) => Promise<string | null>;
}
/**
* 媒体缓存 Hook
* @description 提供缓存统计、清理功能、访问记录等
* @returns {UseMediaCacheReturn} 缓存管理接口
*
* @example
* ```tsx
* function MediaSettings() {
* const { stats, cleanup, formatSize } = useMediaCache();
*
* return (
* <View>
* <Text>总缓存: {formatSize(stats?.totalSize || 0)}</Text>
* <Text>图片: {stats?.imageCount || 0}</Text>
* <Text>视频: {stats?.videoCount || 0}</Text>
* <Button title="清理缓存" onPress={cleanup} />
* </View>
* );
* }
* ```
*/
export function useMediaCache(): UseMediaCacheReturn {
const [stats, setStats] = useState<CacheStats | null>(null);
const [isClearing, setIsClearing] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
const initializedRef = useRef(false);
/**
* 加载缓存统计
*/
const loadStats = useCallback(() => {
const cacheStats = mediaCacheManager.getCacheStats();
setStats(cacheStats);
}, []);
/**
* 执行清理
*/
const cleanup = useCallback(async (policy?: Partial<CleanupPolicy>): Promise<CleanupResult> => {
setIsClearing(true);
try {
// 如果提供了自定义策略,应用它
// 注意:这里简化处理,实际可能需要合并策略
const result = await mediaCacheManager.cleanup(CleanupTrigger.MANUAL);
await loadStats();
return result;
} finally {
setIsClearing(false);
}
}, [loadStats]);
/**
* 清空所有缓存
*/
const clearAll = useCallback(async (): Promise<void> => {
setIsClearing(true);
try {
await mediaCacheManager.clearAll();
await loadStats();
} finally {
setIsClearing(false);
}
}, [loadStats]);
/**
* 清理指定会话的缓存
*/
const clearConversation = useCallback(async (conversationId: string): Promise<CleanupResult> => {
setIsClearing(true);
try {
const result = await mediaCacheManager.clearConversationMedia(conversationId);
await loadStats();
return result;
} finally {
setIsClearing(false);
}
}, [loadStats]);
/**
* 启动定期清理
*/
const startPeriodicCleanup = useCallback(() => {
mediaCacheManager.startPeriodicCleanup();
}, []);
/**
* 停止定期清理
*/
const stopPeriodicCleanup = useCallback(() => {
mediaCacheManager.stopPeriodicCleanup();
}, []);
/**
* 记录缓存访问
*/
const recordAccess = useCallback(async (key: string): Promise<void> => {
await mediaCacheManager.recordAccess(key);
}, []);
/**
* 缓存媒体文件
*/
const cacheMedia = useCallback(async (
uri: string,
type: MediaType,
options?: {
conversationId?: string;
messageId?: string;
size?: number;
}
): Promise<string> => {
const localPath = await mediaCacheManager.cacheMedia(uri, type, options);
await loadStats();
return localPath;
}, [loadStats]);
/**
* 获取缓存的媒体路径
*/
const getCachedMedia = useCallback(async (uri: string, type: MediaType): Promise<string | null> => {
return await mediaCacheManager.getCachedMedia(uri, type);
}, []);
/**
* 格式化缓存大小
*/
const formatSize = useCallback((bytes: number): string => {
if (bytes < 1024) {
return `${bytes} B`;
}
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
}
if (bytes < 1024 * 1024 * 1024) {
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
}, []);
/**
* 初始化
*/
useEffect(() => {
const initialize = async () => {
if (initializedRef.current) return;
initializedRef.current = true;
try {
await mediaCacheManager.initialize();
setIsInitialized(true);
loadStats();
} catch (error) {
console.error('[useMediaCache] 初始化失败:', error);
// 即使初始化失败,也允许使用基本功能
setIsInitialized(true);
loadStats();
}
};
initialize();
// 清理函数
return () => {
// 停止定期清理
mediaCacheManager.stopPeriodicCleanup();
};
}, [loadStats]);
return {
stats,
isClearing,
isInitialized,
loadStats,
cleanup,
clearAll,
clearConversation,
startPeriodicCleanup,
stopPeriodicCleanup,
recordAccess,
formatSize,
cacheMedia,
getCachedMedia,
};
}

321
src/hooks/usePagination.ts Normal file
View File

@@ -0,0 +1,321 @@
/**
* 分页 Hook
*
* 提供 React 组件中使用的分页功能,包括:
* - loadMore: 加载更多数据
* - refresh: 刷新数据
* - prefetch: 预加载数据
* - 自动预加载检测
* - 分页状态管理
*/
import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
import {
paginationStateManager,
PaginationState,
PaginationConfig,
LoadMoreResult,
RefreshResult,
PrefetchResult,
FetchPageFunction,
DEFAULT_PAGINATION_CONFIG,
} from '../infrastructure/pagination';
/**
* usePagination 选项
*/
export interface UsePaginationOptions<T = any> {
/** 分页键通常是会话ID或列表标识 */
key: string | null;
/** 数据获取函数 */
fetchFunction: FetchPageFunction<T>;
/** 分页配置 */
config?: Partial<PaginationConfig>;
/** 初始数据 */
initialData?: T[];
/** 是否自动加载第一页 */
autoLoad?: boolean;
/** 是否启用自动预加载 */
enableAutoPrefetch?: boolean;
}
/**
* usePagination 返回值
*/
export interface UsePaginationReturn<T = any> {
/** 当前数据列表 */
data: T[];
/** 是否正在加载 */
isLoading: boolean;
/** 是否正在刷新 */
isRefreshing: boolean;
/** 是否有更多数据 */
hasMore: boolean;
/** 是否发生过错误 */
hasError: boolean;
/** 错误信息 */
error?: string;
/** 当前页码 */
currentPage: number;
/** 已加载的总数 */
totalLoaded: number;
/** 加载更多 */
loadMore: () => Promise<LoadMoreResult<T>>;
/** 刷新数据 */
refresh: () => Promise<RefreshResult<T>>;
/** 预加载指定页面 */
prefetch: (page: number) => Promise<PrefetchResult<T>>;
/** 重置分页状态 */
reset: () => void;
/** 手动设置数据 */
setData: (data: T[]) => void;
/** 追加数据 */
appendData: (data: T[]) => void;
}
/**
* 分页 Hook
*
* 示例用法:
* ```typescript
* const {
* data,
* isLoading,
* hasMore,
* loadMore,
* refresh,
* } = usePagination<Message>({
* key: conversationId,
* fetchFunction: async (page, pageSize, cursor) => {
* const result = await messageService.getMessages({
* page,
* pageSize,
* beforeSeq: cursor,
* });
* return {
* data: result.messages,
* hasMore: result.has_more,
* cursor: result.min_seq,
* };
* },
* config: { pageSize: 20 },
* });
* ```
*/
export function usePagination<T = any>(
options: UsePaginationOptions<T>
): UsePaginationReturn<T> {
const {
key,
fetchFunction,
config: userConfig,
initialData = [],
autoLoad = false,
enableAutoPrefetch = true,
} = options;
// 合并配置
const config = useMemo(() => ({
...DEFAULT_PAGINATION_CONFIG,
...userConfig,
}), [userConfig]);
// 内部数据状态
const [data, setData] = useState<T[]>(initialData);
const [isRefreshing, setIsRefreshing] = useState(false);
const [lastError, setLastError] = useState<string | undefined>(undefined);
// 用于追踪是否已自动加载
const autoLoadRef = useRef(false);
// 用于追踪组件是否已卸载
const isMountedRef = useRef(true);
// 获取当前分页状态
const paginationState = useMemo(() => {
if (!key) return null;
return paginationStateManager.getState(key, config);
}, [key, config]);
// 计算派生状态
const isLoading = paginationState?.isLoading ?? false;
const hasMore = paginationState?.hasMore ?? true;
const currentPage = paginationState?.currentPage ?? 0;
const totalLoaded = paginationState?.totalLoaded ?? 0;
const hasError = paginationState?.hasError ?? false;
const error = paginationState?.error ?? lastError;
// 当 key 变化时重置数据
useEffect(() => {
if (key) {
setData(initialData);
autoLoadRef.current = false;
}
}, [key]);
// 组件卸载时清理
useEffect(() => {
return () => {
isMountedRef.current = false;
};
}, []);
// 自动加载第一页
useEffect(() => {
if (!key || !autoLoad || autoLoadRef.current) return;
if (data.length > 0) return;
autoLoadRef.current = true;
refresh();
}, [key, autoLoad, data.length]);
// 自动预加载检测
useEffect(() => {
if (!key || !enableAutoPrefetch) return;
if (!paginationStateManager.shouldPrefetch(key, data.length)) return;
// 触发预加载
const prefetchNextPage = async () => {
const nextPage = currentPage + 1;
await paginationStateManager.prefetch(key, nextPage, fetchFunction);
};
prefetchNextPage();
}, [key, data.length, currentPage, enableAutoPrefetch, fetchFunction]);
/**
* 加载更多数据
*/
const loadMore = useCallback(async (): Promise<LoadMoreResult<T>> => {
if (!key) {
return {
success: false,
data: [],
hasMore: false,
fromCache: false,
error: 'No pagination key provided',
};
}
const result = await paginationStateManager.loadMore<T>(key, fetchFunction);
if (isMountedRef.current) {
if (result.success) {
setData(prev => [...prev, ...result.data]);
setLastError(undefined);
} else if (result.error) {
setLastError(result.error);
}
}
return result;
}, [key, fetchFunction]);
/**
* 刷新数据
*/
const refresh = useCallback(async (): Promise<RefreshResult<T>> => {
if (!key) {
return {
success: false,
data: [],
hasMore: false,
error: 'No pagination key provided',
};
}
setIsRefreshing(true);
try {
const result = await paginationStateManager.refresh<T>(key, fetchFunction);
if (isMountedRef.current) {
if (result.success) {
setData(result.data);
setLastError(undefined);
} else if (result.error) {
setLastError(result.error);
}
}
return result;
} finally {
if (isMountedRef.current) {
setIsRefreshing(false);
}
}
}, [key, fetchFunction]);
/**
* 预加载指定页面
*/
const prefetch = useCallback(async (page: number): Promise<PrefetchResult<T>> => {
if (!key) {
return {
success: false,
error: 'No pagination key provided',
};
}
return paginationStateManager.prefetch<T>(key, page, fetchFunction);
}, [key, fetchFunction]);
/**
* 重置分页状态
*/
const reset = useCallback(() => {
if (!key) return;
paginationStateManager.reset(key, config);
if (isMountedRef.current) {
setData([]);
setLastError(undefined);
autoLoadRef.current = false;
}
}, [key, config]);
/**
* 手动设置数据
*/
const setDataManual = useCallback((newData: T[]) => {
if (isMountedRef.current) {
setData(newData);
}
}, []);
/**
* 追加数据
*/
const appendData = useCallback((newData: T[]) => {
if (isMountedRef.current) {
setData(prev => [...prev, ...newData]);
}
}, []);
return {
data,
isLoading,
isRefreshing,
hasMore,
hasError,
error,
currentPage,
totalLoaded,
loadMore,
refresh,
prefetch,
reset,
setData: setDataManual,
appendData,
};
}
/**
* 使用分页状态管理器的单例实例
* 用于在组件外访问分页状态
*/
export function usePaginationManager() {
return paginationStateManager;
}
export default usePagination;

View File

@@ -0,0 +1,726 @@
/**
* 媒体缓存管理器
* @module src/infrastructure/cache/MediaCacheManager
* @description 管理图片、视频、音频缓存,支持多种清理策略
*/
import { File, Paths } from 'expo-file-system';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {
MediaType,
CleanupTrigger,
CacheEntry,
CacheStats,
CleanupPolicy,
CleanupResult,
MediaCacheConfig,
DEFAULT_MEDIA_CACHE_CONFIG,
DEFAULT_CLEANUP_POLICY,
} from './types';
/**
* 缓存记录存储键前缀
*/
const CACHE_RECORD_PREFIX = 'media_cache_record_';
/**
* 获取缓存目录路径
*/
function getCacheDir(): string {
const cachePath = Paths.cache;
return `${cachePath}media_cache/`;
}
/**
* 获取图片缓存目录
*/
function getImageDir(): string {
return `${getCacheDir()}images/`;
}
/**
* 获取视频缓存目录
*/
function getVideoDir(): string {
return `${getCacheDir()}videos/`;
}
/**
* 获取音频缓存目录
*/
function getAudioDir(): string {
return `${getCacheDir()}audios/`;
}
/**
* 媒体缓存管理器类
* @class MediaCacheManager
* @description 单例模式,管理所有媒体文件的缓存
*/
export class MediaCacheManager {
private static instance: MediaCacheManager;
private cache: Map<string, CacheEntry> = new Map();
private config: MediaCacheConfig;
private policy: CleanupPolicy;
private cleanupTimer: ReturnType<typeof setTimeout> | null = null;
private lastCleanupTime: number = 0;
private isInitialized: boolean = false;
/**
* 获取单例实例
*/
public static getInstance(): MediaCacheManager {
if (!MediaCacheManager.instance) {
MediaCacheManager.instance = new MediaCacheManager();
}
return MediaCacheManager.instance;
}
/**
* 私有构造函数
*/
private constructor(
config?: Partial<MediaCacheConfig>,
policy?: Partial<CleanupPolicy>
) {
this.config = { ...DEFAULT_MEDIA_CACHE_CONFIG, ...config };
this.policy = { ...DEFAULT_CLEANUP_POLICY, ...policy };
}
/**
* 初始化缓存管理器
* 确保缓存目录存在并加载缓存记录
*/
public async initialize(): Promise<void> {
if (this.isInitialized) return;
try {
// 确保缓存目录存在
await this.ensureDirectories();
// 加载缓存记录
await this.loadCacheRecords();
// 启动时清理
if (this.policy.cleanupOnStart) {
await this.cleanup(CleanupTrigger.STARTUP);
}
// 启动定期清理
this.startPeriodicCleanup();
this.isInitialized = true;
} catch (error) {
console.error('[MediaCacheManager] 初始化失败:', error);
throw error;
}
}
/**
* 确保缓存目录存在
*/
private async ensureDirectories(): Promise<void> {
try {
const dirs = [getImageDir(), getVideoDir(), getAudioDir()];
for (const dir of dirs) {
const dirFile = new File(dir);
if (!await dirFile.exists) {
await dirFile.create();
}
}
} catch (error) {
console.error('[MediaCacheManager] 创建缓存目录失败:', error);
}
}
/**
* 从持久化存储加载缓存记录
*/
private async loadCacheRecords(): Promise<void> {
try {
const keys = await AsyncStorage.getAllKeys();
const recordKeys = keys.filter(k => k.startsWith(CACHE_RECORD_PREFIX));
if (recordKeys.length === 0) return;
const records = await AsyncStorage.multiGet(recordKeys);
for (const [key, value] of records) {
if (value) {
const entry: CacheEntry = JSON.parse(value);
// 验证文件是否存在
const exists = await this.checkFileExists(entry.localPath);
if (exists) {
this.cache.set(entry.key, entry);
} else {
// 文件不存在,删除记录
await AsyncStorage.removeItem(key);
}
}
}
} catch (error) {
console.error('[MediaCacheManager] 加载缓存记录失败:', error);
}
}
/**
* 检查文件是否存在
*/
private async checkFileExists(path: string): Promise<boolean> {
try {
const file = new File(path);
return await file.exists;
} catch {
return false;
}
}
/**
* 记录缓存访问用于LRU
*/
public async recordAccess(key: string): Promise<void> {
const entry = this.cache.get(key);
if (entry) {
entry.lastAccessedAt = Date.now();
await this.saveCacheRecord(entry);
}
}
/**
* 缓存媒体文件
*/
public async cacheMedia(
uri: string,
type: MediaType,
options: {
conversationId?: string;
messageId?: string;
size?: number;
} = {}
): Promise<string> {
const { conversationId, messageId, size = 0 } = options;
// 生成唯一键
const key = this.generateCacheKey(uri, type);
const localPath = this.getMediaPath(type, key);
// 检查是否已缓存
const existingEntry = this.cache.get(key);
if (existingEntry) {
await this.recordAccess(key);
return existingEntry.localPath;
}
try {
// 下载文件
const destination = new File(localPath);
const downloaded = await File.downloadFileAsync(uri, destination);
// 获取实际文件大小
let fileSize = size;
// fileSize 为 0 时使用默认大小,实际应用中可以通过其他方式获取
// 创建缓存条目
const entry: CacheEntry = {
key,
type,
uri,
localPath: downloaded.uri,
size: fileSize,
conversationId,
messageId,
createdAt: Date.now(),
lastAccessedAt: Date.now(),
};
// 保存记录
this.cache.set(key, entry);
await this.saveCacheRecord(entry);
// 检查是否需要清理
await this.checkAndCleanup();
return downloaded.uri;
} catch (error) {
console.error('[MediaCacheManager] 缓存媒体失败:', error);
throw error;
}
}
/**
* 获取缓存的媒体路径
*/
public async getCachedMedia(uri: string, type: MediaType): Promise<string | null> {
const key = this.generateCacheKey(uri, type);
const entry = this.cache.get(key);
if (entry) {
const exists = await this.checkFileExists(entry.localPath);
if (exists) {
await this.recordAccess(key);
return entry.localPath;
} else {
// 文件不存在,删除记录
this.cache.delete(key);
await this.removeCacheRecord(key);
}
}
return null;
}
/**
* 获取缓存统计信息
*/
public getCacheStats(): CacheStats {
const stats: CacheStats = {
totalSize: 0,
imageCount: 0,
videoCount: 0,
audioCount: 0,
totalEntries: this.cache.size,
oldestItem: Date.now(),
newestItem: 0,
};
for (const entry of this.cache.values()) {
stats.totalSize += entry.size;
stats.oldestItem = Math.min(stats.oldestItem, entry.createdAt);
stats.newestItem = Math.max(stats.newestItem, entry.createdAt);
switch (entry.type) {
case MediaType.IMAGE:
stats.imageCount++;
break;
case MediaType.VIDEO:
stats.videoCount++;
break;
case MediaType.AUDIO:
stats.audioCount++;
break;
}
}
return stats;
}
/**
* 清理操作入口
*/
public async cleanup(trigger: CleanupTrigger): Promise<CleanupResult> {
const errors: string[] = [];
let deletedCount = 0;
let freedSize = 0;
const deletedItems: string[] = [];
try {
// 1. 清理过期缓存
const expiredResult = await this.cleanupExpired();
deletedCount += expiredResult.deletedCount;
freedSize += expiredResult.freedSize;
deletedItems.push(...expiredResult.deletedItems || []);
errors.push(...expiredResult.errors);
// 2. LRU清理如果启用
if (this.policy.enableLRU) {
const lruResult = await this.cleanupLRU();
deletedCount += lruResult.deletedCount;
freedSize += lruResult.freedSize;
deletedItems.push(...lruResult.deletedItems || []);
errors.push(...lruResult.errors);
}
// 3. 阈值清理
const thresholdResult = await this.cleanupByThreshold();
deletedCount += thresholdResult.deletedCount;
freedSize += thresholdResult.freedSize;
deletedItems.push(...thresholdResult.deletedItems || []);
errors.push(...thresholdResult.errors);
this.lastCleanupTime = Date.now();
} catch (error) {
errors.push(`清理失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
return {
trigger,
timestamp: Date.now(),
deletedCount,
freedSize,
errors,
deletedItems,
};
}
/**
* 清理过期缓存
*/
public async cleanupExpired(): Promise<CleanupResult> {
const now = Date.now();
const maxAge = this.policy.maxAge;
const errors: string[] = [];
let deletedCount = 0;
let freedSize = 0;
const deletedItems: string[] = [];
const entriesToDelete: string[] = [];
for (const [key, entry] of this.cache.entries()) {
if (now - entry.lastAccessedAt > maxAge) {
entriesToDelete.push(key);
}
}
for (const key of entriesToDelete) {
try {
const entry = this.cache.get(key);
if (entry) {
const file = new File(entry.localPath);
if (await file.exists) {
await file.delete();
}
this.cache.delete(key);
await this.removeCacheRecord(key);
deletedCount++;
freedSize += entry.size;
deletedItems.push(key);
}
} catch (error) {
errors.push(`删除 ${key} 失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
return {
trigger: CleanupTrigger.MANUAL,
timestamp: Date.now(),
deletedCount,
freedSize,
errors,
deletedItems,
};
}
/**
* LRU清理 - 删除最近最少使用的缓存
*/
public async cleanupLRU(targetSize?: number): Promise<CleanupResult> {
const errors: string[] = [];
let deletedCount = 0;
let freedSize = 0;
const deletedItems: string[] = [];
// 如果没有指定目标大小,使用最大缓存大小的一半
const target = targetSize || Math.floor(this.policy.maxSize / 2);
const currentStats = this.getCacheStats();
if (currentStats.totalSize <= target) {
return {
trigger: CleanupTrigger.MANUAL,
timestamp: Date.now(),
deletedCount: 0,
freedSize: 0,
errors: [],
deletedItems: [],
};
}
// 按最后访问时间排序
const sortedEntries = Array.from(this.cache.entries())
.sort((a, b) => a[1].lastAccessedAt - b[1].lastAccessedAt);
let currentSize = currentStats.totalSize;
for (const [key, entry] of sortedEntries) {
if (currentSize <= target) break;
try {
const file = new File(entry.localPath);
if (await file.exists) {
await file.delete();
}
this.cache.delete(key);
await this.removeCacheRecord(key);
deletedCount++;
freedSize += entry.size;
deletedItems.push(key);
currentSize -= entry.size;
} catch (error) {
errors.push(`删除 ${key} 失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
return {
trigger: CleanupTrigger.MANUAL,
timestamp: Date.now(),
deletedCount,
freedSize,
errors,
deletedItems,
};
}
/**
* 按阈值清理 - 超过最大大小时清理
*/
private async cleanupByThreshold(): Promise<CleanupResult> {
const currentStats = this.getCacheStats();
const errors: string[] = [];
let deletedCount = 0;
let freedSize = 0;
const deletedItems: string[] = [];
// 检查总大小是否超过限制
if (currentStats.totalSize <= this.policy.maxSize) {
// 检查条目数是否超过限制
if (currentStats.totalEntries <= this.policy.maxEntries) {
return {
trigger: CleanupTrigger.MANUAL,
timestamp: Date.now(),
deletedCount: 0,
freedSize: 0,
errors: [],
deletedItems: [],
};
}
}
// 需要清理
const targetSize = Math.floor(this.policy.maxSize * 0.8); // 清理到80%
const targetEntries = Math.floor(this.policy.maxEntries * 0.8);
// 按LRU排序
const sortedEntries = Array.from(this.cache.entries())
.sort((a, b) => a[1].lastAccessedAt - b[1].lastAccessedAt);
let currentSize = currentStats.totalSize;
let currentEntries = currentStats.totalEntries;
for (const [key, entry] of sortedEntries) {
// 两个条件:大小或条目数
if (currentSize <= targetSize && currentEntries <= targetEntries) break;
try {
const file = new File(entry.localPath);
if (await file.exists) {
await file.delete();
}
this.cache.delete(key);
await this.removeCacheRecord(key);
deletedCount++;
freedSize += entry.size;
deletedItems.push(key);
currentSize -= entry.size;
currentEntries--;
} catch (error) {
errors.push(`删除 ${key} 失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
return {
trigger: CleanupTrigger.MANUAL,
timestamp: Date.now(),
deletedCount,
freedSize,
errors,
deletedItems,
};
}
/**
* 删除指定会话的所有媒体缓存
*/
public async clearConversationMedia(conversationId: string): Promise<CleanupResult> {
const errors: string[] = [];
let deletedCount = 0;
let freedSize = 0;
const deletedItems: string[] = [];
const entriesToDelete: string[] = [];
for (const [key, entry] of this.cache.entries()) {
if (entry.conversationId === conversationId) {
entriesToDelete.push(key);
}
}
for (const key of entriesToDelete) {
try {
const entry = this.cache.get(key);
if (entry) {
const file = new File(entry.localPath);
if (await file.exists) {
await file.delete();
}
this.cache.delete(key);
await this.removeCacheRecord(key);
deletedCount++;
freedSize += entry.size;
deletedItems.push(key);
}
} catch (error) {
errors.push(`删除 ${key} 失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
return {
trigger: CleanupTrigger.CONVERSATION_DELETED,
timestamp: Date.now(),
deletedCount,
freedSize,
errors,
deletedItems,
};
}
/**
* 清空所有缓存
*/
public async clearAll(): Promise<void> {
for (const entry of this.cache.values()) {
try {
const file = new File(entry.localPath);
if (await file.exists) {
await file.delete();
}
} catch (error) {
console.warn(`[MediaCacheManager] 删除文件失败: ${entry.localPath}`, error);
}
}
this.cache.clear();
// 清除所有记录
const keys = await AsyncStorage.getAllKeys();
const recordKeys = keys.filter(k => k.startsWith(CACHE_RECORD_PREFIX));
if (recordKeys.length > 0) {
await AsyncStorage.multiRemove(recordKeys);
}
}
/**
* 启动定期清理
*/
public startPeriodicCleanup(): void {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
}
this.cleanupTimer = setInterval(async () => {
// 检查是否需要清理
if (Date.now() - this.lastCleanupTime >= this.policy.cleanupInterval) {
await this.cleanup(CleanupTrigger.SCHEDULED);
}
}, this.policy.cleanupInterval);
}
/**
* 停止定期清理
*/
public stopPeriodicCleanup(): void {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = null;
}
}
/**
* 检查是否需要清理
*/
private async checkAndCleanup(): Promise<void> {
const stats = this.getCacheStats();
// 检查大小
if (stats.totalSize > this.policy.maxSize) {
await this.cleanupLRU();
}
// 检查条目数
if (stats.totalEntries > this.policy.maxEntries) {
await this.cleanupLRU();
}
}
/**
* 保存缓存记录到持久化存储
*/
private async saveCacheRecord(entry: CacheEntry): Promise<void> {
try {
const key = `${CACHE_RECORD_PREFIX}${entry.key}`;
await AsyncStorage.setItem(key, JSON.stringify(entry));
} catch (error) {
console.error('[MediaCacheManager] 保存缓存记录失败:', error);
}
}
/**
* 删除缓存记录
*/
private async removeCacheRecord(key: string): Promise<void> {
try {
const storageKey = `${CACHE_RECORD_PREFIX}${key}`;
await AsyncStorage.removeItem(storageKey);
} catch (error) {
console.error('[MediaCacheManager] 删除缓存记录失败:', error);
}
}
/**
* 生成缓存键
*/
private generateCacheKey(uri: string, type: MediaType): string {
const hash = this.hashString(uri);
return `${type.toLowerCase()}_${hash}_${Date.now()}`;
}
/**
* 获取媒体文件存储路径
*/
private getMediaPath(type: MediaType, key: string): string {
const ext = this.getExtension(key, type);
switch (type) {
case MediaType.IMAGE:
return `${getImageDir()}${key}.${ext}`;
case MediaType.VIDEO:
return `${getVideoDir()}${key}.${ext}`;
case MediaType.AUDIO:
return `${getAudioDir()}${key}.${ext}`;
}
}
/**
* 根据类型获取文件扩展名
*/
private getExtension(key: string, type: MediaType): string {
if (type === MediaType.IMAGE) {
if (key.includes('gif')) return 'gif';
if (key.includes('webp')) return 'webp';
return 'jpg';
}
if (type === MediaType.VIDEO) return 'mp4';
if (type === MediaType.AUDIO) return 'mp3';
return 'bin';
}
/**
* 字符串哈希函数
*/
private hashString(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(16);
}
}
/**
* 导出单例实例
*/
export const mediaCacheManager = MediaCacheManager.getInstance();

21
src/infrastructure/cache/index.ts vendored Normal file
View File

@@ -0,0 +1,21 @@
/**
* 媒体缓存模块导出
* @module src/infrastructure/cache
* @description 导出媒体缓存相关的所有类型和管理器
*/
// 类型导出
export {
MediaType,
CleanupTrigger,
CacheEntry,
CacheStats,
CleanupPolicy,
CleanupResult,
MediaCacheConfig,
DEFAULT_MEDIA_CACHE_CONFIG,
DEFAULT_CLEANUP_POLICY,
} from './types';
// 管理器导出
export { MediaCacheManager, mediaCacheManager } from './MediaCacheManager';

202
src/infrastructure/cache/types.ts vendored Normal file
View File

@@ -0,0 +1,202 @@
/**
* 媒体缓存类型定义
* @module src/infrastructure/cache/types
* @description 定义媒体缓存相关的类型、枚举和接口
*/
/**
* 媒体类型枚举
* @enum {string}
* @description 支持的媒体类型
*/
export enum MediaType {
IMAGE = 'IMAGE',
VIDEO = 'VIDEO',
AUDIO = 'AUDIO',
}
/**
* 清理触发器枚举
* @enum {string}
* @description 触发清理操作的来源
*/
export enum CleanupTrigger {
STARTUP = 'startup',
SCHEDULED = 'scheduled',
MANUAL = 'manual',
LOW_STORAGE = 'low_storage',
CONVERSATION_DELETED = 'conversation_deleted',
}
/**
* 缓存条目接口
* @interface CacheEntry
* @description 单个媒体缓存条目的信息
*/
export interface CacheEntry {
/** 缓存键值 */
key: string;
/** 媒体类型 */
type: MediaType;
/** 原始URI */
uri: string;
/** 本地存储路径 */
localPath: string;
/** 文件大小(字节) */
size: number;
/** 所属会话ID(可选) */
conversationId?: string;
/** 关联消息ID(可选) */
messageId?: string;
/** 创建时间 */
createdAt: number;
/** 最后访问时间 */
lastAccessedAt: number;
}
/**
* 缓存统计接口
* @interface CacheStats
* @description 媒体缓存的整体统计信息
*/
export interface CacheStats {
/** 总缓存大小(字节) */
totalSize: number;
/** 图片缓存数量 */
imageCount: number;
/** 视频缓存数量 */
videoCount: number;
/** 音频缓存数量 */
audioCount: number;
/** 总条目数 */
totalEntries: number;
/** 最早缓存项时间 */
oldestItem: number;
/** 最新缓存项时间 */
newestItem: number;
}
/**
* 清理策略接口
* @interface CleanupPolicy
* @description 定义缓存清理的规则和限制
*/
export interface CleanupPolicy {
/** 最大缓存时间(毫秒) */
maxAge: number;
/** 最大缓存大小(字节) */
maxSize: number;
/** 最大条目数 */
maxEntries: number;
/** 启动时是否清理 */
cleanupOnStart: boolean;
/** 定期清理间隔(毫秒) */
cleanupInterval: number;
/** 是否启用LRU清理 */
enableLRU: boolean;
/** 低存储空间阈值(字节) */
lowStorageThreshold: number;
}
/**
* 清理结果接口
* @interface CleanupResult
* @description 清理操作的执行结果
*/
export interface CleanupResult {
/** 触发器类型 */
trigger: CleanupTrigger;
/** 执行时间戳 */
timestamp: number;
/** 删除条目数量 */
deletedCount: number;
/** 释放空间大小(字节) */
freedSize: number;
/** 错误信息列表 */
errors: string[];
/** 清理的条目详情 */
deletedItems?: string[];
}
/**
* 媒体缓存配置接口
* @interface MediaCacheConfig
* @description 媒体缓存的全局配置
*/
export interface MediaCacheConfig {
/** 图片配置 */
image: {
/** 内存缓存数量 */
maxMemoryCacheSize: number;
/** 磁盘缓存大小(MB) */
maxDiskCacheSize: number;
/** 缓存有效期(小时) */
maxAge: number;
/** 会话内媒体保留时间(天) */
maxAgeForConversation: number;
};
/** 视频配置 */
video: {
/** 磁盘缓存大小(MB) */
maxDiskCacheSize: number;
/** 缓存有效期(小时) */
maxAge: number;
/** 是否自动预加载 */
autoPreload: boolean;
};
/** 音频配置 */
audio: {
/** 磁盘缓存大小(MB) */
maxDiskCacheSize: number;
/** 缓存有效期(天) */
maxAge: number;
};
/** 全局配置 */
global: {
/** 启动时检查清理 */
checkOnStartup: boolean;
/** 定期检查间隔(小时) */
checkInterval: number;
/** 低存储空间阈值(MB) */
lowStorageThreshold: number;
};
}
/**
* 默认媒体缓存配置
*/
export const DEFAULT_MEDIA_CACHE_CONFIG: MediaCacheConfig = {
image: {
maxMemoryCacheSize: 50,
maxDiskCacheSize: 500,
maxAge: 168, // 7 days
maxAgeForConversation: 30,
},
video: {
maxDiskCacheSize: 1000,
maxAge: 72, // 3 days
autoPreload: false,
},
audio: {
maxDiskCacheSize: 200,
maxAge: 168, // 7 days
},
global: {
checkOnStartup: true,
checkInterval: 24,
lowStorageThreshold: 500,
},
};
/**
* 默认清理策略
*/
export const DEFAULT_CLEANUP_POLICY: CleanupPolicy = {
maxAge: 7 * 24 * 60 * 60 * 1000, // 7天
maxSize: 500 * 1024 * 1024, // 500MB
maxEntries: 1000,
cleanupOnStart: true,
cleanupInterval: 24 * 60 * 60 * 1000, // 24小时
enableLRU: true,
lowStorageThreshold: 500 * 1024 * 1024, // 500MB
};

View File

@@ -0,0 +1,268 @@
/**
* 消息差异计算器
* 计算两个消息列表之间的差异,支持增量更新
*/
import {
MessageIdentifier,
DiffResult,
MessageComparator,
MessageSorter,
DiffConfig,
DEFAULT_DIFF_CONFIG,
} from './types';
/**
* 消息差异计算器类
* 用于高效计算消息列表的变化
*/
export class MessageDiffCalculator<T extends MessageIdentifier> {
private config: Required<DiffConfig>;
private previousMessages: Map<string, T> = new Map();
private previousOrder: string[] = [];
constructor(config: DiffConfig = {}) {
this.config = { ...DEFAULT_DIFF_CONFIG, ...config };
}
/**
* 计算两个消息列表的差异
* @param oldList 旧消息列表
* @param newList 新消息列表
* @returns 差异结果
*/
calculateDiff(oldList: T[], newList: T[]): DiffResult<T> {
const result: DiffResult<T> = {
added: [],
updated: [],
deleted: [],
moved: [],
unchanged: [],
shouldReset: false,
};
// 如果旧列表为空,直接返回新列表作为新增
if (oldList.length === 0) {
result.added = [...newList];
this.updateCache(newList);
return result;
}
// 如果新列表为空,表示全部删除
if (newList.length === 0) {
result.deleted = oldList.map((msg, index) => ({ message: msg, index }));
this.clearCache();
return result;
}
// 检查是否需要重置(变化太大)
const changeRatio = this.calculateChangeRatio(oldList, newList);
if (changeRatio > 0.5) {
result.shouldReset = true;
result.added = [...newList];
this.updateCache(newList);
return result;
}
// 构建旧列表的索引映射
const oldIndexMap = new Map<string, number>();
oldList.forEach((msg, index) => {
oldIndexMap.set(msg.id, index);
});
// 构建新列表的索引映射
const newIndexMap = new Map<string, number>();
newList.forEach((msg, index) => {
newIndexMap.set(msg.id, index);
});
// 检测新增、更新和未变更的消息
const processedIds = new Set<string>();
for (let newIndex = 0; newIndex < newList.length; newIndex++) {
const newMsg = newList[newIndex];
const oldIndex = oldIndexMap.get(newMsg.id);
if (oldIndex === undefined) {
// 新增消息
result.added.push(newMsg);
} else {
const oldMsg = oldList[oldIndex];
processedIds.add(newMsg.id);
// 检测是否有更新
const changes = this.detectChanges(oldMsg, newMsg);
if (changes && Object.keys(changes).length > 0) {
result.updated.push({
message: newMsg,
index: oldIndex,
changes,
});
} else {
result.unchanged.push(newMsg);
}
// 检测是否移动
if (oldIndex !== newIndex) {
result.moved.push({
message: newMsg,
fromIndex: oldIndex,
toIndex: newIndex,
});
}
}
}
// 检测删除的消息
for (let oldIndex = 0; oldIndex < oldList.length; oldIndex++) {
const oldMsg = oldList[oldIndex];
if (!processedIds.has(oldMsg.id)) {
result.deleted.push({ message: oldMsg, index: oldIndex });
}
}
this.updateCache(newList);
return result;
}
/**
* 计算增量差异(基于缓存的上次状态)
* @param newList 新消息列表
* @returns 差异结果
*/
calculateIncrementalDiff(newList: T[]): DiffResult<T> {
const oldList = Array.from(this.previousMessages.values());
return this.calculateDiff(oldList, newList);
}
/**
* 计算变化比例
* @param oldList 旧列表
* @param newList 新列表
* @returns 变化比例0-1
*/
private calculateChangeRatio(oldList: T[], newList: T[]): number {
if (oldList.length === 0 && newList.length === 0) return 0;
if (oldList.length === 0 || newList.length === 0) return 1;
const oldIds = new Set(oldList.map(m => m.id));
const newIds = new Set(newList.map(m => m.id));
let commonCount = 0;
oldIds.forEach(id => {
if (newIds.has(id)) {
commonCount++;
}
});
const maxLength = Math.max(oldList.length, newList.length);
return 1 - commonCount / maxLength;
}
/**
* 检测消息变化
* @param oldMsg 旧消息
* @param newMsg 新消息
* @returns 变化字段
*/
private detectChanges(oldMsg: T, newMsg: T): Partial<T> | null {
const changes: Partial<T> = {};
let hasChanges = false;
for (const key of Object.keys(newMsg) as Array<keyof T>) {
if (key === 'id') continue; // ID 不变
const oldValue = oldMsg[key];
const newValue = newMsg[key];
if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
(changes as any)[key] = newValue;
hasChanges = true;
}
}
return hasChanges ? changes : null;
}
/**
* 更新缓存
* @param messages 消息列表
*/
private updateCache(messages: T[]): void {
this.previousMessages.clear();
this.previousOrder = [];
for (const msg of messages) {
this.previousMessages.set(msg.id, msg);
this.previousOrder.push(msg.id);
}
}
/**
* 清除缓存
*/
private clearCache(): void {
this.previousMessages.clear();
this.previousOrder = [];
}
/**
* 设置配置
* @param config 配置
*/
setConfig(config: Partial<DiffConfig>): void {
this.config = { ...this.config, ...config };
}
/**
* 获取当前配置
* @returns 当前配置
*/
getConfig(): Required<DiffConfig> {
return { ...this.config };
}
/**
* 重置计算器状态
*/
reset(): void {
this.clearCache();
}
/**
* 获取缓存的消息数量
* @returns 缓存数量
*/
getCachedCount(): number {
return this.previousMessages.size;
}
/**
* 检查消息是否在缓存中
* @param messageId 消息 ID
* @returns 是否存在
*/
hasMessage(messageId: string): boolean {
return this.previousMessages.has(messageId);
}
/**
* 获取缓存中的消息
* @param messageId 消息 ID
* @returns 消息或 undefined
*/
getCachedMessage(messageId: string): T | undefined {
return this.previousMessages.get(messageId);
}
}
/**
* 创建差异计算器实例的工厂函数
* @param config 配置
* @returns MessageDiffCalculator 实例
*/
export function createMessageDiffCalculator<T extends MessageIdentifier>(
config?: DiffConfig
): MessageDiffCalculator<T> {
return new MessageDiffCalculator<T>(config);
}

View File

@@ -0,0 +1,385 @@
/**
* 消息更新批量处理器
* 收集更新操作并按批处理,减少 UI 更新次数
*/
import {
MessageUpdate,
MessageUpdateType,
BaseMessageUpdate,
BatchUpdateListener,
DiffConfig,
DEFAULT_DIFF_CONFIG,
MessageIdentifier,
} from './types';
/**
* 批量处理器配置选项
*/
export interface BatcherOptions {
/** 批量处理间隔(毫秒),默认 16ms约 60fps */
batchInterval?: number;
/** 最大批量大小,默认 100 */
maxBatchSize?: number;
/** 是否启用去重,默认 true */
enableDeduplication?: boolean;
/** 是否合并相同消息的多次更新,默认 true */
enableMerge?: boolean;
/** 最大等待时间(毫秒),超过此时间强制刷新,默认 100ms */
maxWaitTime?: number;
/** 是否自动启动批量处理,默认 true */
autoStart?: boolean;
}
/**
* 默认批量处理器配置
*/
export const DEFAULT_BATCHER_OPTIONS: Required<BatcherOptions> = {
batchInterval: 16,
maxBatchSize: 100,
enableDeduplication: true,
enableMerge: true,
maxWaitTime: 100,
autoStart: true,
};
/**
* 消息更新批量处理器类
*/
export class MessageUpdateBatcher {
/** 待处理的更新队列 */
private pendingUpdates: Map<string, MessageUpdate> = new Map();
/** 批量处理定时器 */
private batchTimer: ReturnType<typeof setTimeout> | null = null;
/** 最大等待时间定时器 */
private maxWaitTimer: ReturnType<typeof setTimeout> | null = null;
/** 监听器集合 */
private listeners: Set<BatchUpdateListener> = new Set();
/** 是否正在批处理中 */
private isProcessing: boolean = false;
/** 配置选项 */
private options: Required<BatcherOptions>;
/** 处理器是否已启动 */
private isStarted: boolean = false;
/** 批量处理统计信息 */
private stats: {
totalBatches: number;
totalUpdates: number;
averageBatchSize: number;
lastBatchTime: number;
} = {
totalBatches: 0,
totalUpdates: 0,
averageBatchSize: 0,
lastBatchTime: 0,
};
constructor(options: BatcherOptions = {}) {
this.options = { ...DEFAULT_BATCHER_OPTIONS, ...options };
if (this.options.autoStart) {
this.start();
}
}
/**
* 启动批量处理器
*/
start(): void {
if (this.isStarted) return;
this.isStarted = true;
this.scheduleBatch();
}
/**
* 停止批量处理器
*/
stop(): void {
this.isStarted = false;
this.clearTimers();
}
/**
* 添加更新操作到批量队列
* @param update 消息更新操作
*/
addUpdate(update: MessageUpdate): void {
const key = this.generateUpdateKey(update);
// 去重:如果已有相同消息 ID 的待处理更新,进行合并
if (this.options.enableDeduplication && this.pendingUpdates.has(key)) {
const existing = this.pendingUpdates.get(key)!;
if (this.options.enableMerge) {
const merged = this.mergeUpdates(existing, update);
if (merged) {
this.pendingUpdates.set(key, merged);
return;
}
}
}
this.pendingUpdates.set(key, update);
// 如果达到最大批量大小,立即刷新
if (this.pendingUpdates.size >= this.options.maxBatchSize) {
this.flush();
}
}
/**
* 添加多个更新操作
* @param updates 更新操作数组
*/
addUpdates(updates: MessageUpdate[]): void {
for (const update of updates) {
this.addUpdate(update);
}
}
/**
* 立即刷新所有待处理更新
* @returns 处理的更新数组
*/
flush(): MessageUpdate[] {
if (this.pendingUpdates.size === 0 || this.isProcessing) {
return [];
}
this.isProcessing = true;
this.clearTimers();
const updates = Array.from(this.pendingUpdates.values());
this.pendingUpdates.clear();
// 更新统计信息
this.stats.totalBatches++;
this.stats.totalUpdates += updates.length;
this.stats.averageBatchSize = this.stats.totalUpdates / this.stats.totalBatches;
this.stats.lastBatchTime = Date.now();
// 通知监听器
this.notifyListeners(updates);
this.isProcessing = false;
// 重新调度定时器
this.scheduleBatch();
return updates;
}
/**
* 订阅批量更新事件
* @param listener 监听器函数
* @returns 取消订阅函数
*/
subscribe(listener: BatchUpdateListener): () => void {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
/**
* 取消订阅
* @param listener 监听器函数
*/
unsubscribe(listener: BatchUpdateListener): void {
this.listeners.delete(listener);
}
/**
* 获取待处理更新数量
* @returns 待处理数量
*/
getPendingCount(): number {
return this.pendingUpdates.size;
}
/**
* 获取统计信息
* @returns 统计信息
*/
getStats() {
return { ...this.stats };
}
/**
* 检查是否有待处理更新
* @returns 是否有待处理更新
*/
hasPendingUpdates(): boolean {
return this.pendingUpdates.size > 0;
}
/**
* 清空所有待处理更新
*/
clearPending(): void {
this.pendingUpdates.clear();
this.clearTimers();
}
/**
* 销毁批量处理器
*/
destroy(): void {
this.stop();
this.clearPending();
this.listeners.clear();
}
/**
* 生成更新键值
* @param update 更新操作
* @returns 键值
*/
private generateUpdateKey(update: MessageUpdate): string {
// 根据更新类型生成唯一键
switch (update.type) {
case 'ADD':
case 'BATCH_ADD':
return `${update.type}_${update.timestamp}`;
case 'UPDATE':
case 'BATCH_UPDATE':
return `${update.type}_${this.extractMessageId(update)}`;
case 'DELETE':
case 'BATCH_DELETE':
return `${update.type}_${this.extractMessageId(update)}`;
default:
return `${update.type}_${update.timestamp}_${Math.random()}`;
}
}
/**
* 提取消息 ID
* @param update 更新操作
* @returns 消息 ID 或空字符串
*/
private extractMessageId(update: MessageUpdate): string {
const payload = (update as any).payload;
if (payload) {
if (payload.messageId) return payload.messageId;
if (payload.messageIds) return payload.messageIds.join(',');
}
return '';
}
/**
* 合并更新操作
* @param existing 已有更新
* @param incoming 新更新
* @returns 合并后的更新或 null如果无法合并
*/
private mergeUpdates(existing: MessageUpdate, incoming: MessageUpdate): MessageUpdate | null {
// 只有同类型的更新才能合并
if (existing.type !== incoming.type) {
return null;
}
switch (existing.type) {
case 'UPDATE':
// 合并更新字段
return {
...existing,
payload: {
...(existing as any).payload,
updates: {
...(existing as any).payload?.updates,
...(incoming as any).payload?.updates,
},
},
timestamp: incoming.timestamp,
};
case 'BATCH_UPDATE':
// 合并批量更新
const existingUpdates = (existing as any).payload?.updates || [];
const incomingUpdates = (incoming as any).payload?.updates || [];
const updatesMap = new Map();
for (const update of [...existingUpdates, ...incomingUpdates]) {
const existing = updatesMap.get(update.messageId);
if (existing) {
existing.changes = { ...existing.changes, ...update.changes };
} else {
updatesMap.set(update.messageId, { ...update });
}
}
return {
...existing,
payload: {
...(existing as any).payload,
updates: Array.from(updatesMap.values()),
},
timestamp: incoming.timestamp,
};
default:
return null;
}
}
/**
* 调度批量处理
*/
private scheduleBatch(): void {
if (!this.isStarted) return;
this.clearTimers();
// 设置最大等待时间定时器
this.maxWaitTimer = setTimeout(() => {
this.flush();
}, this.options.maxWaitTime);
// 设置批量处理定时器
this.batchTimer = setTimeout(() => {
this.flush();
}, this.options.batchInterval);
}
/**
* 清除所有定时器
*/
private clearTimers(): void {
if (this.batchTimer) {
clearTimeout(this.batchTimer);
this.batchTimer = null;
}
if (this.maxWaitTimer) {
clearTimeout(this.maxWaitTimer);
this.maxWaitTimer = null;
}
}
/**
* 通知监听器
* @param updates 更新数组
*/
private notifyListeners(updates: MessageUpdate[]): void {
if (this.listeners.size === 0) return;
// 转换为数组以支持迭代
const listenersArray = Array.from(this.listeners);
for (const listener of listenersArray) {
try {
listener(updates);
} catch (error) {
console.error('Error in batch update listener:', error);
}
}
}
}
/**
* 创建批量处理器的工厂函数
* @param options 配置选项
* @returns MessageUpdateBatcher 实例
*/
export function createMessageUpdateBatcher(
options?: BatcherOptions
): MessageUpdateBatcher {
return new MessageUpdateBatcher(options);
}

View File

@@ -0,0 +1,40 @@
/**
* 差异更新模块
* 提供消息列表的增量更新功能,减少 UI 重渲染次数
*/
// 类型定义
export {
MessageUpdateType,
MessageIdentifier,
BaseMessageUpdate,
AddMessageUpdate,
UpdateMessageUpdate,
DeleteMessageUpdate,
MoveMessageUpdate,
BatchAddMessageUpdate,
BatchUpdateMessageUpdate,
BatchDeleteMessageUpdate,
ResetMessageUpdate,
MessageUpdate,
DiffConfig,
DiffResult,
BatchUpdateListener,
MessageComparator,
MessageSorter,
DEFAULT_DIFF_CONFIG,
} from './types';
// 差异计算器
export {
MessageDiffCalculator,
createMessageDiffCalculator,
} from './MessageDiffCalculator';
// 批量处理器
export {
MessageUpdateBatcher,
createMessageUpdateBatcher,
BatcherOptions,
DEFAULT_BATCHER_OPTIONS,
} from './MessageUpdateBatcher';

View File

@@ -0,0 +1,209 @@
/**
* 差异更新类型定义
* 用于消息列表的增量更新,减少 UI 重渲染次数
*/
/**
* 消息更新类型枚举
*/
export enum MessageUpdateType {
/** 添加单条消息 */
ADD = 'ADD',
/** 更新单条消息 */
UPDATE = 'UPDATE',
/** 删除单条消息 */
DELETE = 'DELETE',
/** 移动消息位置 */
MOVE = 'MOVE',
/** 批量添加 */
BATCH_ADD = 'BATCH_ADD',
/** 批量更新 */
BATCH_UPDATE = 'BATCH_UPDATE',
/** 批量删除 */
BATCH_DELETE = 'BATCH_DELETE',
/** 重置整个列表 */
RESET = 'RESET',
}
/**
* 消息唯一标识接口
*/
export interface MessageIdentifier {
/** 消息唯一 ID */
id: string;
/** 消息序列号(可选,用于排序) */
seq?: number;
}
/**
* 基础更新操作接口
*/
export interface BaseMessageUpdate {
/** 更新类型 */
type: MessageUpdateType;
/** 更新时间戳 */
timestamp: number;
/** 会话 ID可选 */
conversationId?: string;
/** 载荷数据(可选,用于合并更新) */
payload?: Record<string, any>;
}
/**
* 添加单条消息更新
*/
export interface AddMessageUpdate extends BaseMessageUpdate {
type: MessageUpdateType.ADD;
/** 消息数据 */
message: MessageIdentifier;
/** 插入位置索引(可选,默认追加到末尾) */
index?: number;
}
/**
* 更新单条消息更新
*/
export interface UpdateMessageUpdate extends BaseMessageUpdate {
type: MessageUpdateType.UPDATE;
/** 消息 ID */
messageId: string;
/** 更新的字段 */
updates: Partial<MessageIdentifier>;
}
/**
* 删除单条消息更新
*/
export interface DeleteMessageUpdate extends BaseMessageUpdate {
type: MessageUpdateType.DELETE;
/** 消息 ID */
messageId: string;
}
/**
* 移动消息更新
*/
export interface MoveMessageUpdate extends BaseMessageUpdate {
type: MessageUpdateType.MOVE;
/** 消息 ID */
messageId: string;
/** 目标位置索引 */
toIndex: number;
}
/**
* 批量添加消息更新
*/
export interface BatchAddMessageUpdate extends BaseMessageUpdate {
type: MessageUpdateType.BATCH_ADD;
/** 消息列表 */
messages: MessageIdentifier[];
/** 插入位置索引(可选,默认追加到末尾) */
startIndex?: number;
}
/**
* 批量更新消息更新
*/
export interface BatchUpdateMessageUpdate extends BaseMessageUpdate {
type: MessageUpdateType.BATCH_UPDATE;
/** 批量更新项 */
updates: Array<{
messageId: string;
changes: Partial<MessageIdentifier>;
}>;
}
/**
* 批量删除消息更新
*/
export interface BatchDeleteMessageUpdate extends BaseMessageUpdate {
type: MessageUpdateType.BATCH_DELETE;
/** 要删除的消息 ID 列表 */
messageIds: string[];
}
/**
* 重置消息列表更新
*/
export interface ResetMessageUpdate extends BaseMessageUpdate {
type: MessageUpdateType.RESET;
/** 新的消息列表 */
messages: MessageIdentifier[];
}
/**
* 消息更新联合类型
*/
export type MessageUpdate =
| AddMessageUpdate
| UpdateMessageUpdate
| DeleteMessageUpdate
| MoveMessageUpdate
| BatchAddMessageUpdate
| BatchUpdateMessageUpdate
| BatchDeleteMessageUpdate
| ResetMessageUpdate;
/**
* 差异配置接口
*/
export interface DiffConfig {
/** 批量处理间隔(毫秒),默认 16ms60fps */
batchInterval?: number;
/** 最大批量大小,默认 100 */
maxBatchSize?: number;
/** 是否启用去重,默认 true */
enableDeduplication?: boolean;
/** 是否合并相同消息的多次更新,默认 true */
enableMerge?: boolean;
/** 消息唯一标识字段,默认 'id' */
idField?: string;
/** 排序字段,默认 'seq' */
sortField?: string;
}
/**
* 差异计算结果接口
*/
export interface DiffResult<T extends MessageIdentifier> {
/** 新增的消息 */
added: T[];
/** 更新的消息 */
updated: Array<{ message: T; index: number; changes: Partial<T> }>;
/** 删除的消息 */
deleted: Array<{ message: T; index: number }>;
/** 移动的消息 */
moved: Array<{ message: T; fromIndex: number; toIndex: number }>;
/** 未变更的消息 */
unchanged: T[];
/** 是否需要重置 */
shouldReset: boolean;
}
/**
* 批量更新监听器类型
*/
export type BatchUpdateListener = (updates: MessageUpdate[]) => void;
/**
* 消息比较函数类型
*/
export type MessageComparator<T extends MessageIdentifier> = (a: T, b: T) => boolean;
/**
* 消息排序函数类型
*/
export type MessageSorter<T extends MessageIdentifier> = (a: T, b: T) => number;
/**
* 默认配置常量
*/
export const DEFAULT_DIFF_CONFIG: Required<DiffConfig> = {
batchInterval: 16,
maxBatchSize: 100,
enableDeduplication: true,
enableMerge: true,
idField: 'id',
sortField: 'seq',
};

View File

@@ -0,0 +1,546 @@
/**
* 分页状态管理器
*
* 提供分页状态的集中管理,包括:
* - 分页状态跟踪(当前页、总加载数、是否有更多等)
* - 页面缓存机制(避免重复加载相同页面)
* - 加载锁机制(防止并发重复请求)
* - 预加载功能(提前加载下一页数据)
*/
import {
PaginationState,
PageCache,
PaginationConfig,
LoadMoreResult,
RefreshResult,
PrefetchResult,
FetchPageFunction,
DEFAULT_PAGINATION_CONFIG,
createInitialPaginationState,
createPageCache,
isCacheExpired,
} from './types';
/**
* 分页状态管理器类
* 管理多个分页键的状态,支持页面缓存和加载锁
*/
export class PaginationStateManager {
// 分页状态存储key -> PaginationState
private states: Map<string, PaginationState> = new Map();
// 页面缓存存储key -> Map<pageNumber, PageCache>
private pageCaches: Map<string, Map<number, PageCache>> = new Map();
// 加载锁存储key -> Set<pageNumber>
private loadingLocks: Map<string, Set<number>> = new Map();
// 配置存储key -> PaginationConfig
private configs: Map<string, PaginationConfig> = new Map();
/**
* 获取或创建分页状态
* @param key 分页键通常是会话ID或列表标识
* @param config 可选的配置
* @returns 分页状态
*/
getState(key: string, config?: Partial<PaginationConfig>): PaginationState {
let state = this.states.get(key);
if (!state) {
const mergedConfig = this.getConfig(key, config);
state = createInitialPaginationState(mergedConfig.pageSize);
this.states.set(key, state);
}
return state;
}
/**
* 更新分页状态
* @param key 分页键
* @param partialState 部分状态更新
*/
setState(key: string, partialState: Partial<PaginationState>): void {
const currentState = this.getState(key);
this.states.set(key, {
...currentState,
...partialState,
});
}
/**
* 获取配置
* @param key 分页键
* @param overrideConfig 可选的覆盖配置
* @returns 合并后的配置
*/
getConfig(key: string, overrideConfig?: Partial<PaginationConfig>): PaginationConfig {
const existingConfig = this.configs.get(key);
if (overrideConfig) {
const mergedConfig = {
...(existingConfig || DEFAULT_PAGINATION_CONFIG),
...overrideConfig,
};
this.configs.set(key, mergedConfig);
return mergedConfig;
}
return existingConfig || DEFAULT_PAGINATION_CONFIG;
}
/**
* 尝试获取加载锁
* @param key 分页键
* @param page 页码
* @returns 是否成功获取锁
*/
acquireLock(key: string, page: number): boolean {
let locks = this.loadingLocks.get(key);
if (!locks) {
locks = new Set();
this.loadingLocks.set(key, locks);
}
// 如果已经在加载中,返回 false
if (locks.has(page)) {
return false;
}
// 获取锁成功
locks.add(page);
// 更新状态为加载中
this.setState(key, { isLoading: true, hasError: false, error: undefined });
return true;
}
/**
* 释放加载锁
* @param key 分页键
* @param page 页码
*/
releaseLock(key: string, page: number): void {
const locks = this.loadingLocks.get(key);
if (locks) {
locks.delete(page);
// 如果没有正在加载的页面了,更新状态
if (locks.size === 0) {
this.setState(key, { isLoading: false });
}
}
}
/**
* 检查是否正在加载指定页面
* @param key 分页键
* @param page 页码
* @returns 是否正在加载
*/
isPageLoading(key: string, page: number): boolean {
const locks = this.loadingLocks.get(key);
return locks ? locks.has(page) : false;
}
/**
* 检查是否有任何页面正在加载
* @param key 分页键
* @returns 是否有页面正在加载
*/
isAnyPageLoading(key: string): boolean {
const locks = this.loadingLocks.get(key);
return locks ? locks.size > 0 : false;
}
/**
* 缓存页面数据
* @param key 分页键
* @param page 页码
* @param data 页面数据
* @param cursor 可选的分页游标
*/
cachePage<T>(key: string, page: number, data: T[], cursor?: string | number | null): void {
const config = this.getConfig(key);
if (!config.enableCache) return;
let caches = this.pageCaches.get(key);
if (!caches) {
caches = new Map();
this.pageCaches.set(key, caches);
}
// 如果超过最大缓存页数,清理最旧的缓存
if (caches.size >= config.maxCachedPages) {
this.cleanupOldestCache(key);
}
caches.set(page, createPageCache(page, data, cursor));
}
/**
* 获取缓存的页面数据
* @param key 分页键
* @param page 页码
* @returns 缓存数据或 null
*/
getCachedPage<T>(key: string, page: number): PageCache<T> | null {
const config = this.getConfig(key);
if (!config.enableCache) return null;
const caches = this.pageCaches.get(key);
if (!caches) return null;
const cache = caches.get(page);
if (!cache) return null;
// 检查缓存是否过期
if (isCacheExpired(cache, config.cacheTtl)) {
caches.delete(page);
return null;
}
return cache as PageCache<T>;
}
/**
* 检查页面是否已缓存且未过期
* @param key 分页键
* @param page 页码
* @returns 是否已缓存
*/
isPageCached(key: string, page: number): boolean {
return this.getCachedPage(key, page) !== null;
}
/**
* 清除指定键的所有缓存
* @param key 分页键
*/
clearCache(key: string): void {
this.pageCaches.delete(key);
}
/**
* 重置分页状态
* @param key 分页键
* @param config 可选的新配置
*/
reset(key: string, config?: Partial<PaginationConfig>): void {
const mergedConfig = this.getConfig(key, config);
this.states.set(key, createInitialPaginationState(mergedConfig.pageSize));
this.clearCache(key);
this.loadingLocks.delete(key);
}
/**
* 删除分页状态
* @param key 分页键
*/
remove(key: string): void {
this.states.delete(key);
this.pageCaches.delete(key);
this.loadingLocks.delete(key);
this.configs.delete(key);
}
/**
* 清理所有数据
*/
clear(): void {
this.states.clear();
this.pageCaches.clear();
this.loadingLocks.clear();
this.configs.clear();
}
/**
* 获取所有分页键
* @returns 分页键数组
*/
getKeys(): string[] {
return Array.from(this.states.keys());
}
/**
* 清理最旧的缓存
* @param key 分页键
*/
private cleanupOldestCache(key: string): void {
const caches = this.pageCaches.get(key);
if (!caches || caches.size === 0) return;
let oldestPage = -1;
let oldestTime = Infinity;
Array.from(caches.entries()).forEach(([page, cache]) => {
if (cache.cachedAt < oldestTime) {
oldestTime = cache.cachedAt;
oldestPage = page;
}
});
if (oldestPage >= 0) {
caches.delete(oldestPage);
}
}
/**
* 加载更多数据
* @param key 分页键
* @param fetchFunction 数据获取函数
* @returns 加载结果
*/
async loadMore<T>(
key: string,
fetchFunction: FetchPageFunction<T>
): Promise<LoadMoreResult<T>> {
const state = this.getState(key);
const config = this.getConfig(key);
const nextPage = state.currentPage + 1;
// 检查是否还有更多数据
if (!state.hasMore) {
return {
success: true,
data: [],
hasMore: false,
fromCache: false,
};
}
// 尝试获取加载锁
if (!this.acquireLock(key, nextPage)) {
return {
success: false,
data: [],
hasMore: state.hasMore,
fromCache: false,
error: 'Page is already loading',
};
}
try {
// 检查缓存
const cachedPage = this.getCachedPage<T>(key, nextPage);
if (cachedPage) {
// 更新状态
this.setState(key, {
currentPage: nextPage,
totalLoaded: state.totalLoaded + cachedPage.data.length,
lastLoadTime: Date.now(),
});
return {
success: true,
data: cachedPage.data,
hasMore: state.hasMore,
fromCache: true,
};
}
// 从数据源加载
const result = await fetchFunction(nextPage, state.pageSize, state.cursor);
// 缓存页面数据
if (config.enableCache && result.data.length > 0) {
this.cachePage(key, nextPage, result.data, result.cursor);
}
// 更新状态
this.setState(key, {
currentPage: nextPage,
totalLoaded: state.totalLoaded + result.data.length,
hasMore: result.hasMore,
cursor: result.cursor,
lastLoadTime: Date.now(),
hasError: false,
error: undefined,
});
return {
success: true,
data: result.data,
hasMore: result.hasMore,
fromCache: false,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.setState(key, {
hasError: true,
error: errorMessage,
});
return {
success: false,
data: [],
hasMore: state.hasMore,
fromCache: false,
error: errorMessage,
};
} finally {
this.releaseLock(key, nextPage);
}
}
/**
* 刷新数据(重新加载第一页)
* @param key 分页键
* @param fetchFunction 数据获取函数
* @returns 刷新结果
*/
async refresh<T>(
key: string,
fetchFunction: FetchPageFunction<T>
): Promise<RefreshResult<T>> {
const state = this.getState(key);
const config = this.getConfig(key);
// 尝试获取加载锁
if (!this.acquireLock(key, 1)) {
return {
success: false,
data: [],
hasMore: state.hasMore,
error: 'Refresh already in progress',
};
}
try {
// 清除缓存
this.clearCache(key);
// 重置状态
this.setState(key, {
currentPage: 0,
totalLoaded: 0,
hasMore: true,
cursor: null,
hasError: false,
error: undefined,
});
// 加载第一页
const result = await fetchFunction(1, state.pageSize, null);
// 缓存第一页
if (config.enableCache && result.data.length > 0) {
this.cachePage(key, 1, result.data, result.cursor);
}
// 更新状态
this.setState(key, {
currentPage: 1,
totalLoaded: result.data.length,
hasMore: result.hasMore,
cursor: result.cursor,
lastLoadTime: Date.now(),
});
return {
success: true,
data: result.data,
hasMore: result.hasMore,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.setState(key, {
hasError: true,
error: errorMessage,
});
return {
success: false,
data: [],
hasMore: state.hasMore,
error: errorMessage,
};
} finally {
this.releaseLock(key, 1);
}
}
/**
* 预加载指定页面
* @param key 分页键
* @param page 页码
* @param fetchFunction 数据获取函数
* @returns 预加载结果
*/
async prefetch<T>(
key: string,
page: number,
fetchFunction: FetchPageFunction<T>
): Promise<PrefetchResult<T>> {
const config = this.getConfig(key);
// 检查是否启用预加载
if (!config.enablePrefetch) {
return { success: false, error: 'Prefetch is disabled' };
}
// 检查是否已缓存
if (this.isPageCached(key, page)) {
return { success: true, data: this.getCachedPage<T>(key, page)?.data };
}
// 检查是否正在加载
if (this.isPageLoading(key, page)) {
return { success: false, error: 'Page is already loading' };
}
// 尝试获取加载锁
if (!this.acquireLock(key, page)) {
return { success: false, error: 'Failed to acquire lock' };
}
try {
const result = await fetchFunction(page, config.pageSize, null);
// 缓存数据
if (config.enableCache && result.data.length > 0) {
this.cachePage(key, page, result.data, result.cursor);
}
return {
success: true,
data: result.data,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
success: false,
error: errorMessage,
};
} finally {
this.releaseLock(key, page);
}
}
/**
* 检查是否应该预加载下一页
* @param key 分页键
* @param currentItemCount 当前已加载的条目数
* @returns 是否应该预加载
*/
shouldPrefetch(key: string, currentItemCount: number): boolean {
const state = this.getState(key);
const config = this.getConfig(key);
if (!config.enablePrefetch) return false;
if (!state.hasMore) return false;
if (state.isLoading) return false;
const nextPage = state.currentPage + 1;
if (this.isPageLoading(key, nextPage)) return false;
if (this.isPageCached(key, nextPage)) return false;
// 检查是否达到预加载阈值
const threshold = state.totalLoaded - config.prefetchThreshold;
return currentItemCount >= threshold;
}
}
// 导出单例实例
export const paginationStateManager = new PaginationStateManager();

View File

@@ -0,0 +1,34 @@
/**
* 分页状态管理模块
*
* 提供分页状态管理的核心功能,包括:
* - PaginationStateManager: 分页状态管理器类
* - paginationStateManager: 单例实例
* - usePagination: React Hook 用于在组件中使用分页功能
* - 类型定义和工具函数
*/
// 导出类型定义
export type {
PaginationState,
PageCache,
PaginationConfig,
LoadMoreResult,
RefreshResult,
PrefetchResult,
FetchPageFunction,
} from './types';
// 导出工具函数和常量
export {
DEFAULT_PAGINATION_CONFIG,
createInitialPaginationState,
createPageCache,
isCacheExpired,
} from './types';
// 导出分页状态管理器
export {
PaginationStateManager,
paginationStateManager,
} from './PaginationStateManager';

View File

@@ -0,0 +1,182 @@
/**
* 分页状态管理类型定义
*
* 提供分页相关的接口和类型定义
* 用于统一管理分页状态、缓存和配置
*/
/**
* 分页状态
* 跟踪特定分页键的分页信息
*/
export interface PaginationState {
/** 当前页码1-based */
currentPage: number;
/** 每页大小 */
pageSize: number;
/** 已加载的总条目数 */
totalLoaded: number;
/** 是否存在更多数据 */
hasMore: boolean;
/** 是否正在加载 */
isLoading: boolean;
/** 最后加载时间戳 */
lastLoadTime: number;
/** 分页游标(用于基于游标的分页) */
cursor?: string | number | null;
/** 是否发生过错误 */
hasError: boolean;
/** 错误信息 */
error?: string;
}
/**
* 页面缓存
* 存储已加载页面的数据
*/
export interface PageCache<T = any> {
/** 页码 */
page: number;
/** 该页的数据 */
data: T[];
/** 缓存时间戳 */
cachedAt: number;
/** 分页游标 */
cursor?: string | number | null;
}
/**
* 分页配置
* 配置分页行为
*/
export interface PaginationConfig {
/** 每页默认大小 */
pageSize: number;
/** 预加载阈值(距离底部多少条时触发预加载) */
prefetchThreshold: number;
/** 最大缓存页数 */
maxCachedPages: number;
/** 缓存过期时间(毫秒) */
cacheTtl: number;
/** 是否启用预加载 */
enablePrefetch: boolean;
/** 是否启用缓存 */
enableCache: boolean;
/** 最大重试次数 */
maxRetries: number;
/** 重试延迟(毫秒) */
retryDelay: number;
}
/**
* 加载更多结果
* loadMore 方法的返回类型
*/
export interface LoadMoreResult<T = any> {
/** 是否成功 */
success: boolean;
/** 加载的数据 */
data: T[];
/** 是否有更多数据 */
hasMore: boolean;
/** 是否来自缓存 */
fromCache: boolean;
/** 错误信息(如果失败) */
error?: string;
}
/**
* 刷新结果
* refresh 方法的返回类型
*/
export interface RefreshResult<T = any> {
/** 是否成功 */
success: boolean;
/** 刷新后的数据 */
data: T[];
/** 是否有更多数据 */
hasMore: boolean;
/** 错误信息(如果失败) */
error?: string;
}
/**
* 预加载结果
* prefetch 方法的返回类型
*/
export interface PrefetchResult<T = any> {
/** 是否成功 */
success: boolean;
/** 预加载的数据 */
data?: T[];
/** 错误信息(如果失败) */
error?: string;
}
/**
* 分页数据加载函数
* 用于从数据源加载分页数据
*/
export type FetchPageFunction<T = any> = (
page: number,
pageSize: number,
cursor?: string | number | null
) => Promise<{
data: T[];
hasMore: boolean;
cursor?: string | number | null;
}>;
/**
* 默认分页配置
*/
export const DEFAULT_PAGINATION_CONFIG: PaginationConfig = {
pageSize: 20,
prefetchThreshold: 5,
maxCachedPages: 10,
cacheTtl: 5 * 60 * 1000, // 5分钟
enablePrefetch: true,
enableCache: true,
maxRetries: 3,
retryDelay: 1000,
};
/**
* 初始分页状态
*/
export function createInitialPaginationState(pageSize: number = 20): PaginationState {
return {
currentPage: 0,
pageSize,
totalLoaded: 0,
hasMore: true,
isLoading: false,
lastLoadTime: 0,
cursor: null,
hasError: false,
error: undefined,
};
}
/**
* 创建页面缓存
*/
export function createPageCache<T>(
page: number,
data: T[],
cursor?: string | number | null
): PageCache<T> {
return {
page,
data,
cachedAt: Date.now(),
cursor,
};
}
/**
* 检查缓存是否过期
*/
export function isCacheExpired<T>(cache: PageCache<T>, ttl: number): boolean {
return Date.now() - cache.cachedAt > ttl;
}

View File

@@ -0,0 +1,289 @@
/**
* 连接状态管理器
*
* 提供连接状态的集中管理,包括:
* - 6种连接状态管理IDLE/CONNECTING/CONNECTED/RECONNECTING/DISCONNECTED/ERROR
* - 状态转换逻辑验证
* - 状态订阅机制(监听器模式)
* - 重连次数和最后连接时间跟踪
* - 单例实例提供
*/
import {
ConnectionState,
ConnectionStateInfo,
ConnectionStateListener,
ReconnectConfig,
StateTransitions,
DEFAULT_STATE_TRANSITIONS,
DEFAULT_RECONNECT_CONFIG,
getStateDescription,
} from './types';
/**
* 连接状态管理器类
*
* 管理连接状态的生命周期,提供状态转换、订阅和状态信息查询功能。
* 使用单例模式确保全局唯一实例。
*/
export class ConnectionStateManager {
/** 当前连接状态 */
private state: ConnectionState = ConnectionState.IDLE;
/** 上一次状态 */
private previousState: ConnectionState | null = null;
/** 状态变更监听器集合 */
private listeners: Set<ConnectionStateListener> = new Set();
/** 当前重连次数 */
private reconnectAttempts = 0;
/** 最后成功连接时间 */
private lastConnectedTime: number | null = null;
/** 状态变更时间戳 */
private lastStateChangeTime = Date.now();
/** 当前错误信息 */
private currentError: Error | null = null;
/** 重连配置 */
private reconnectConfig: ReconnectConfig;
/** 状态转换规则 */
private stateTransitions: StateTransitions;
/**
* 创建连接状态管理器实例
* @param config 可选的重连配置
* @param transitions 可选的状态转换规则
*/
constructor(
config: Partial<ReconnectConfig> = {},
transitions: StateTransitions = DEFAULT_STATE_TRANSITIONS
) {
this.reconnectConfig = { ...DEFAULT_RECONNECT_CONFIG, ...config };
this.stateTransitions = transitions;
}
/**
* 获取当前状态信息
* @returns 完整的状态信息对象
*/
getState(): ConnectionStateInfo {
return {
state: this.state,
timestamp: this.lastStateChangeTime,
error: this.currentError || undefined,
reconnectAttempts: this.reconnectAttempts,
lastConnectedAt: this.lastConnectedTime,
description: this.getStatusDescription(),
};
}
/**
* 获取当前原始状态
* @returns 当前连接状态枚举值
*/
getRawState(): ConnectionState {
return this.state;
}
/**
* 获取上一次状态
* @returns 上一次的连接状态,如果没有则为 null
*/
getPreviousState(): ConnectionState | null {
return this.previousState;
}
/**
* 设置新状态
* @param newState 目标状态
* @param error 可选的错误信息
* @returns 是否成功转换状态
*/
setState(newState: ConnectionState, error?: Error): boolean {
// 检查状态转换是否合法
if (!this.canTransitionTo(newState)) {
console.warn(
`[ConnectionStateManager] 非法状态转换: ${this.state} -> ${newState}`
);
return false;
}
// 保存当前状态为上一状态
this.previousState = this.state;
this.state = newState;
this.lastStateChangeTime = Date.now();
// 更新错误信息
if (error) {
this.currentError = error;
} else if (newState !== ConnectionState.ERROR) {
this.currentError = null;
}
// 更新连接时间
if (newState === ConnectionState.CONNECTED) {
this.lastConnectedTime = Date.now();
this.reconnectAttempts = 0;
}
// 通知监听器
this.notifyListeners();
return true;
}
/**
* 检查是否可以转换到目标状态
* @param targetState 目标状态
* @returns 是否可以转换
*/
canTransitionTo(targetState: ConnectionState): boolean {
// 相同状态,允许(刷新状态)
if (this.state === targetState) {
return true;
}
const allowedTransitions = this.stateTransitions[this.state];
if (!allowedTransitions) {
return false;
}
return allowedTransitions.includes(targetState);
}
/**
* 订阅状态变更
* @param listener 状态变更监听器
* @returns 取消订阅函数
*/
subscribe(listener: ConnectionStateListener): () => void {
this.listeners.add(listener);
// 立即通知一次当前状态
listener(this.getState(), null);
// 返回取消订阅函数
return () => {
this.listeners.delete(listener);
};
}
/**
* 获取当前监听器数量
* @returns 监听器数量
*/
getListenerCount(): number {
return this.listeners.size;
}
/**
* 重置重连次数
*/
resetReconnectAttempts(): void {
this.reconnectAttempts = 0;
}
/**
* 增加重连次数
* @returns 增加后的重连次数
*/
incrementReconnectAttempts(): number {
this.reconnectAttempts++;
return this.reconnectAttempts;
}
/**
* 获取当前重连次数
* @returns 当前重连次数
*/
getReconnectAttempts(): number {
return this.reconnectAttempts;
}
/**
* 获取最大重连次数
* @returns 最大重连次数
*/
getMaxReconnectAttempts(): number {
return this.reconnectConfig.maxReconnectAttempts;
}
/**
* 获取最后连接时间
* @returns 最后成功连接的时间戳,如果没有则为 null
*/
getLastConnectedTime(): number | null {
return this.lastConnectedTime;
}
/**
* 获取当前错误信息
* @returns 当前的错误信息,如果没有则为 null
*/
getCurrentError(): Error | null {
return this.currentError;
}
/**
* 检查是否已连接
* @returns 是否处于 CONNECTED 状态
*/
isConnected(): boolean {
return this.state === ConnectionState.CONNECTED;
}
/**
* 检查是否正在连接
* @returns 是否处于 CONNECTING 或 RECONNECTING 状态
*/
isConnecting(): boolean {
return this.state === ConnectionState.CONNECTING || this.state === ConnectionState.RECONNECTING;
}
/**
* 获取状态描述
* @returns 当前状态的描述字符串
*/
getStatusDescription(): string {
return getStateDescription(
this.state,
this.reconnectAttempts,
this.reconnectConfig.maxReconnectAttempts
);
}
/**
* 通知所有监听器状态变更
* @private
*/
private notifyListeners(): void {
const stateInfo = this.getState();
const previousInfo = this.previousState !== null
? { ...stateInfo, state: this.previousState } as ConnectionStateInfo
: null;
this.listeners.forEach((listener) => {
try {
listener(stateInfo, previousInfo);
} catch (error) {
console.error('[ConnectionStateManager] 监听器执行失败:', error);
}
});
}
/**
* 销毁状态管理器
* 清除所有监听器和状态
*/
destroy(): void {
this.listeners.clear();
this.state = ConnectionState.IDLE;
this.previousState = null;
this.reconnectAttempts = 0;
this.currentError = null;
}
}
/**
* 默认单例实例
*/
export const connectionStateManager = new ConnectionStateManager();
export default connectionStateManager;

View File

@@ -0,0 +1,20 @@
/**
* 同步状态管理模块
*
* 提供连接状态管理的统一入口
*/
// 导出类型定义
export {
ConnectionState,
ConnectionStateInfo,
ConnectionStateListener,
ReconnectConfig,
StateTransitions,
DEFAULT_STATE_TRANSITIONS,
DEFAULT_RECONNECT_CONFIG,
getStateDescription,
} from './types';
// 导出状态管理器
export { ConnectionStateManager, connectionStateManager } from './ConnectionStateManager';

View File

@@ -0,0 +1,147 @@
/**
* 同步状态管理类型定义
*
* 提供连接状态相关的类型、接口和枚举
* 用于统一管理 WebSocket/SSE 连接状态
*/
/**
* 连接状态枚举
* 表示连接生命周期的各个阶段
*/
export enum ConnectionState {
/** 初始状态,未开始连接 */
IDLE = 'IDLE',
/** 正在建立连接 */
CONNECTING = 'CONNECTING',
/** 连接已建立 */
CONNECTED = 'CONNECTED',
/** 连接断开,正在重连 */
RECONNECTING = 'RECONNECTING',
/** 连接已断开 */
DISCONNECTED = 'DISCONNECTED',
/** 发生连接错误 */
ERROR = 'ERROR',
}
/**
* 连接状态信息
* 包含完整的状态信息
*/
export interface ConnectionStateInfo {
/** 当前连接状态 */
state: ConnectionState;
/** 状态变更时间戳 */
timestamp: number;
/** 错误信息(仅在 ERROR 状态下有效) */
error?: Error;
/** 当前重连次数(仅在 RECONNECTING 状态下有效) */
reconnectAttempts?: number;
/** 上次成功连接时间 */
lastConnectedAt?: number | null;
/** 状态描述(供显示用) */
description: string;
}
/**
* 连接状态监听器类型
*/
export type ConnectionStateListener = (
state: ConnectionStateInfo,
previousState: ConnectionStateInfo | null
) => void;
/**
* 重连配置接口
*/
export interface ReconnectConfig {
/** 最大重连次数 */
maxReconnectAttempts: number;
/** 初始重连延迟(毫秒) */
initialDelay: number;
/** 最大重连延迟(毫秒) */
maxDelay: number;
/** 延迟倍数(指数退避) */
backoffMultiplier: number;
}
/**
* 状态转换规则
* 定义哪些状态可以转换到哪些状态
*/
export type StateTransitions = {
[key in ConnectionState]?: ConnectionState[];
};
/**
* 默认状态转换规则
*/
export const DEFAULT_STATE_TRANSITIONS: StateTransitions = {
[ConnectionState.IDLE]: [ConnectionState.CONNECTING],
[ConnectionState.CONNECTING]: [
ConnectionState.CONNECTED,
ConnectionState.ERROR,
ConnectionState.DISCONNECTED,
],
[ConnectionState.CONNECTED]: [
ConnectionState.DISCONNECTED,
ConnectionState.ERROR,
ConnectionState.RECONNECTING,
],
[ConnectionState.RECONNECTING]: [
ConnectionState.CONNECTED,
ConnectionState.ERROR,
ConnectionState.DISCONNECTED,
],
[ConnectionState.DISCONNECTED]: [
ConnectionState.CONNECTING,
ConnectionState.RECONNECTING,
],
[ConnectionState.ERROR]: [
ConnectionState.CONNECTING,
ConnectionState.RECONNECTING,
ConnectionState.DISCONNECTED,
],
};
/**
* 默认重连配置
*/
export const DEFAULT_RECONNECT_CONFIG: ReconnectConfig = {
maxReconnectAttempts: 20,
initialDelay: 1000,
maxDelay: 30000,
backoffMultiplier: 2,
};
/**
* 获取状态的中文描述
* @param state 连接状态
* @param reconnectAttempts 当前重连次数
* @param maxReconnectAttempts 最大重连次数
* @returns 状态描述字符串
*/
export function getStateDescription(
state: ConnectionState,
reconnectAttempts?: number,
maxReconnectAttempts?: number
): string {
switch (state) {
case ConnectionState.IDLE:
return '未连接';
case ConnectionState.CONNECTING:
return '正在连接...';
case ConnectionState.CONNECTED:
return '已连接';
case ConnectionState.RECONNECTING:
return reconnectAttempts !== undefined && maxReconnectAttempts !== undefined
? `正在重连 (${reconnectAttempts}/${maxReconnectAttempts})`
: '正在重连...';
case ConnectionState.DISCONNECTED:
return '已断开连接';
case ConnectionState.ERROR:
return '连接错误';
default:
return '未知状态';
}
}

File diff suppressed because it is too large Load Diff