feat(infrastructure): add pagination, cache, and sync infrastructure
- 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:
1725
docs/OPTIMIZATION_DESIGN.md
Normal file
1725
docs/OPTIMIZATION_DESIGN.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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';
|
||||
|
||||
147
src/hooks/useConnectionState.ts
Normal file
147
src/hooks/useConnectionState.ts
Normal 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;
|
||||
369
src/hooks/useDifferentialMessages.ts
Normal file
369
src/hooks/useDifferentialMessages.ts
Normal 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
241
src/hooks/useMediaCache.ts
Normal 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
321
src/hooks/usePagination.ts
Normal 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;
|
||||
726
src/infrastructure/cache/MediaCacheManager.ts
vendored
Normal file
726
src/infrastructure/cache/MediaCacheManager.ts
vendored
Normal 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
21
src/infrastructure/cache/index.ts
vendored
Normal 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
202
src/infrastructure/cache/types.ts
vendored
Normal 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
|
||||
};
|
||||
268
src/infrastructure/diff/MessageDiffCalculator.ts
Normal file
268
src/infrastructure/diff/MessageDiffCalculator.ts
Normal 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);
|
||||
}
|
||||
385
src/infrastructure/diff/MessageUpdateBatcher.ts
Normal file
385
src/infrastructure/diff/MessageUpdateBatcher.ts
Normal 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);
|
||||
}
|
||||
40
src/infrastructure/diff/index.ts
Normal file
40
src/infrastructure/diff/index.ts
Normal 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';
|
||||
209
src/infrastructure/diff/types.ts
Normal file
209
src/infrastructure/diff/types.ts
Normal 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 {
|
||||
/** 批量处理间隔(毫秒),默认 16ms(60fps) */
|
||||
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',
|
||||
};
|
||||
546
src/infrastructure/pagination/PaginationStateManager.ts
Normal file
546
src/infrastructure/pagination/PaginationStateManager.ts
Normal 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();
|
||||
34
src/infrastructure/pagination/index.ts
Normal file
34
src/infrastructure/pagination/index.ts
Normal 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';
|
||||
182
src/infrastructure/pagination/types.ts
Normal file
182
src/infrastructure/pagination/types.ts
Normal 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;
|
||||
}
|
||||
289
src/infrastructure/sync/ConnectionStateManager.ts
Normal file
289
src/infrastructure/sync/ConnectionStateManager.ts
Normal 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;
|
||||
20
src/infrastructure/sync/index.ts
Normal file
20
src/infrastructure/sync/index.ts
Normal 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';
|
||||
147
src/infrastructure/sync/types.ts
Normal file
147
src/infrastructure/sync/types.ts
Normal 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
Reference in New Issue
Block a user