Integrate `expo-callkit-telecom` to support native system call handling (answering, ending, and muting via system UI). This includes a new `callKeepService` and a `CallKeepBootstrap` component to manage the lifecycle of system call events. Additionally, improves the calling experience by: - Adding support for group calls with participant tracking and UI indicators. - Enhancing LiveKit integration by replacing polling with event-driven video track synchronization. - Updating `WebSocketService` to prevent disconnection when the app enters the background during an active call. - Adding new WebSocket message types for participant join/leave events and group invites. - Refining call UI components (`CallScreen`, `FloatingCallWindow`, `IncomingCallModal`) for better visual feedback and safe area handling. Refactor LiveKit service to use event-driven updates for local and remote tracks, improving performance and reliability.
200 lines
5.5 KiB
TypeScript
200 lines
5.5 KiB
TypeScript
/**
|
|
* 缓存数据源实现
|
|
* 基于内存和 AsyncStorage 的混合缓存实现
|
|
*/
|
|
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
import { ICacheDataSource, DataSourceError } from './interfaces';
|
|
|
|
interface CacheEntry<T> {
|
|
value: T;
|
|
expiry: number | null; // null 表示永不过期
|
|
}
|
|
|
|
export class CacheDataSource implements ICacheDataSource {
|
|
private memoryCache: Map<string, CacheEntry<any>> = new Map();
|
|
private maxSize: number;
|
|
private defaultTtl: number; // 默认过期时间(毫秒)
|
|
private persistPrefix: string;
|
|
|
|
constructor(config?: {
|
|
maxSize?: number;
|
|
defaultTtl?: number; // 默认5分钟
|
|
persistPrefix?: string;
|
|
}) {
|
|
this.maxSize = config?.maxSize || 100;
|
|
this.defaultTtl = config?.defaultTtl || 5 * 60 * 1000;
|
|
this.persistPrefix = config?.persistPrefix || 'cache_';
|
|
}
|
|
|
|
/**
|
|
* 检查 key 是否过期
|
|
*/
|
|
private isExpired(entry: CacheEntry<any>): boolean {
|
|
if (entry.expiry === null) return false;
|
|
return Date.now() > entry.expiry;
|
|
}
|
|
|
|
/**
|
|
* 清理过期缓存
|
|
*/
|
|
private cleanup(): void {
|
|
const now = Date.now();
|
|
for (const [key, entry] of this.memoryCache.entries()) {
|
|
if (entry.expiry !== null && now > entry.expiry) {
|
|
this.memoryCache.delete(key);
|
|
}
|
|
}
|
|
|
|
// 如果仍然超过最大大小,删除最旧的条目
|
|
while (this.memoryCache.size > this.maxSize) {
|
|
const firstKey = this.memoryCache.keys().next().value;
|
|
if (firstKey !== undefined) {
|
|
this.memoryCache.delete(firstKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 生成持久化存储 key
|
|
*/
|
|
private getPersistKey(key: string): string {
|
|
return `${this.persistPrefix}${key}`;
|
|
}
|
|
|
|
// ==================== ICacheDataSource 实现 ====================
|
|
|
|
async get<T>(key: string): Promise<T | null> {
|
|
try {
|
|
// 先检查内存缓存
|
|
const memoryEntry = this.memoryCache.get(key);
|
|
if (memoryEntry) {
|
|
if (!this.isExpired(memoryEntry)) {
|
|
return memoryEntry.value as T;
|
|
}
|
|
// 过期了,从内存中删除
|
|
this.memoryCache.delete(key);
|
|
}
|
|
|
|
// 尝试从持久化存储读取
|
|
const persistKey = this.getPersistKey(key);
|
|
const stored = await AsyncStorage.getItem(persistKey);
|
|
if (stored) {
|
|
const entry: CacheEntry<T> = JSON.parse(stored);
|
|
if (!this.isExpired(entry)) {
|
|
// 重新加载到内存缓存
|
|
this.memoryCache.set(key, entry);
|
|
this.cleanup();
|
|
return entry.value;
|
|
}
|
|
// 过期了,删除
|
|
await AsyncStorage.removeItem(persistKey);
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
console.warn(`[CacheDataSource] Failed to get ${key}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
|
|
try {
|
|
const expiry = ttl === undefined
|
|
? (this.defaultTtl > 0 ? Date.now() + this.defaultTtl : null)
|
|
: (ttl > 0 ? Date.now() + ttl : null);
|
|
|
|
const entry: CacheEntry<T> = { value, expiry };
|
|
|
|
// 存入内存
|
|
this.memoryCache.set(key, entry);
|
|
this.cleanup();
|
|
|
|
// 持久化存储(重要数据)
|
|
if (ttl === undefined || ttl > 0) {
|
|
const persistKey = this.getPersistKey(key);
|
|
await AsyncStorage.setItem(persistKey, JSON.stringify(entry));
|
|
}
|
|
} catch (error) {
|
|
throw new DataSourceError(
|
|
`Failed to set cache ${key}`,
|
|
'CACHE_SET_ERROR',
|
|
'CacheDataSource',
|
|
error instanceof Error ? error : undefined
|
|
);
|
|
}
|
|
}
|
|
|
|
async delete(key: string): Promise<void> {
|
|
try {
|
|
// 从内存删除
|
|
this.memoryCache.delete(key);
|
|
|
|
// 从持久化存储删除
|
|
const persistKey = this.getPersistKey(key);
|
|
await AsyncStorage.removeItem(persistKey);
|
|
} catch (error) {
|
|
throw new DataSourceError(
|
|
`Failed to delete cache ${key}`,
|
|
'CACHE_DELETE_ERROR',
|
|
'CacheDataSource',
|
|
error instanceof Error ? error : undefined
|
|
);
|
|
}
|
|
}
|
|
|
|
async clear(): Promise<void> {
|
|
try {
|
|
// 清空内存
|
|
this.memoryCache.clear();
|
|
|
|
// 清空持久化存储中的所有缓存项
|
|
const keys = await AsyncStorage.getAllKeys();
|
|
const cacheKeys = keys.filter(k => k.startsWith(this.persistPrefix));
|
|
if (cacheKeys.length > 0) {
|
|
await AsyncStorage.removeMany(cacheKeys);
|
|
}
|
|
} catch (error) {
|
|
throw new DataSourceError(
|
|
'Failed to clear cache',
|
|
'CACHE_CLEAR_ERROR',
|
|
'CacheDataSource',
|
|
error instanceof Error ? error : undefined
|
|
);
|
|
}
|
|
}
|
|
|
|
async has(key: string): Promise<boolean> {
|
|
try {
|
|
// 检查内存
|
|
const memoryEntry = this.memoryCache.get(key);
|
|
if (memoryEntry && !this.isExpired(memoryEntry)) {
|
|
return true;
|
|
}
|
|
|
|
// 检查持久化存储
|
|
const persistKey = this.getPersistKey(key);
|
|
const stored = await AsyncStorage.getItem(persistKey);
|
|
if (stored) {
|
|
const entry: CacheEntry<any> = JSON.parse(stored);
|
|
return !this.isExpired(entry);
|
|
}
|
|
|
|
return false;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async getMultiple<T>(keys: string[]): Promise<(T | null)[]> {
|
|
return Promise.all(keys.map(key => this.get<T>(key)));
|
|
}
|
|
|
|
async setMultiple<T>(entries: { key: string; value: T; ttl?: number }[]): Promise<void> {
|
|
await Promise.all(entries.map(entry => this.set(entry.key, entry.value, entry.ttl)));
|
|
}
|
|
}
|
|
|
|
// 导出单例实例
|
|
export const cacheDataSource = new CacheDataSource();
|