主要改动: 1. 创建乐观更新工具函数 (optimisticUpdate.ts) - 消除 userStore.ts 中的重复代码 2. 拆分 useResponsive.ts (485行 -> 12个专注模块) - useBreakpoint: 断点检测 - useOrientation: 方向检测 - usePlatform: 平台检测 - useScreenSize: 屏幕尺寸 - useResponsiveValue: 响应式值 - useResponsiveStyle: 响应式样式 - useMediaQuery: 媒体查询 - useColumnCount: 列数计算 - useResponsiveSpacing: 响应式间距 3. 整理数据层 (Repository 层) - ApiDataSource: API数据源 - LocalDataSource: 本地数据源 - CacheDataSource: 缓存数据源 - MessageRepository: 消息仓库 4. 重构 messageManager.ts (2194行 -> 4个模块) - MessageStateManager: 状态管理 - WebSocketMessageHandler: WebSocket处理 - MessageSyncService: 消息同步 - ReadReceiptManager: 已读管理 5. 导航解耦 (MainNavigator.tsx: 1118行 -> 100行) - 创建 NavigationService 解耦层 - 拆分多个 Navigator 组件 架构改进: - 单一职责原则: 每个模块职责明确 - 依赖倒置: 通过接口解耦 - 代码复用: 工具函数可被多处使用 - 可测试性: 各模块可独立测试
282 lines
7.5 KiB
TypeScript
282 lines
7.5 KiB
TypeScript
/**
|
||
* 本地数据库数据源实现
|
||
* 封装所有 SQLite 操作,提供统一的错误处理和事务支持
|
||
*/
|
||
|
||
import * as SQLite from 'expo-sqlite';
|
||
import { ILocalDataSource, DataSourceError } from './interfaces';
|
||
|
||
// 数据库实例管理
|
||
let dbInstance: SQLite.SQLiteDatabase | null = null;
|
||
let currentDbName: string | null = null;
|
||
let writeQueue: Promise<void> = Promise.resolve();
|
||
|
||
export interface LocalDataSourceConfig {
|
||
dbName?: string;
|
||
userId?: string;
|
||
}
|
||
|
||
export class LocalDataSource implements ILocalDataSource {
|
||
private db: SQLite.SQLiteDatabase | null = null;
|
||
private dbName: string;
|
||
private initialized = false;
|
||
|
||
constructor(config: LocalDataSourceConfig = {}) {
|
||
// 如果提供了userId,使用用户专属数据库
|
||
this.dbName = config.dbName || (config.userId ? `carrot_bbs_${config.userId}.db` : 'carrot_bbs.db');
|
||
}
|
||
|
||
/**
|
||
* 初始化数据库连接
|
||
*/
|
||
async initialize(): Promise<void> {
|
||
if (this.initialized && this.db) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 使用全局实例管理
|
||
if (dbInstance && currentDbName === this.dbName) {
|
||
this.db = dbInstance;
|
||
this.initialized = true;
|
||
return;
|
||
}
|
||
|
||
// 关闭旧连接
|
||
if (dbInstance) {
|
||
try {
|
||
await dbInstance.closeAsync();
|
||
} catch (e) {
|
||
console.warn('关闭旧数据库连接失败:', e);
|
||
}
|
||
}
|
||
|
||
// 创建新连接
|
||
this.db = await SQLite.openDatabaseAsync(this.dbName);
|
||
dbInstance = this.db;
|
||
currentDbName = this.dbName;
|
||
this.initialized = true;
|
||
|
||
// 初始化数据库表结构
|
||
await this.createTables();
|
||
} catch (error) {
|
||
this.handleError(error, 'INITIALIZE');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建数据库表结构
|
||
*/
|
||
private async createTables(): Promise<void> {
|
||
if (!this.db) return;
|
||
|
||
const tables = [
|
||
// 消息表
|
||
`CREATE TABLE IF NOT EXISTS messages (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
conversationId TEXT NOT NULL,
|
||
senderId TEXT NOT NULL,
|
||
content TEXT,
|
||
type TEXT DEFAULT 'text',
|
||
isRead INTEGER DEFAULT 0,
|
||
createdAt TEXT NOT NULL,
|
||
seq INTEGER DEFAULT 0,
|
||
status TEXT DEFAULT 'normal',
|
||
segments TEXT
|
||
)`,
|
||
// 会话表
|
||
`CREATE TABLE IF NOT EXISTS conversations (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
participantId TEXT NOT NULL,
|
||
lastMessageId TEXT,
|
||
lastSeq INTEGER DEFAULT 0,
|
||
unreadCount INTEGER DEFAULT 0,
|
||
createdAt TEXT NOT NULL,
|
||
updatedAt TEXT NOT NULL
|
||
)`,
|
||
// 会话缓存表
|
||
`CREATE TABLE IF NOT EXISTS conversation_cache (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
data TEXT NOT NULL,
|
||
updatedAt TEXT NOT NULL
|
||
)`,
|
||
// 会话列表缓存表
|
||
`CREATE TABLE IF NOT EXISTS conversation_list_cache (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
data TEXT NOT NULL,
|
||
updatedAt TEXT NOT NULL
|
||
)`,
|
||
// 用户缓存表
|
||
`CREATE TABLE IF NOT EXISTS users (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
data TEXT NOT NULL,
|
||
updatedAt TEXT NOT NULL
|
||
)`,
|
||
// 当前登录用户缓存
|
||
`CREATE TABLE IF NOT EXISTS current_user_cache (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
data TEXT NOT NULL,
|
||
updatedAt TEXT NOT NULL
|
||
)`,
|
||
// 群组缓存表
|
||
`CREATE TABLE IF NOT EXISTS groups (
|
||
id TEXT PRIMARY KEY NOT NULL,
|
||
data TEXT NOT NULL,
|
||
updatedAt TEXT NOT NULL
|
||
)`,
|
||
// 群成员缓存表
|
||
`CREATE TABLE IF NOT EXISTS group_members (
|
||
groupId TEXT NOT NULL,
|
||
userId TEXT NOT NULL,
|
||
data TEXT NOT NULL,
|
||
updatedAt TEXT NOT NULL,
|
||
PRIMARY KEY (groupId, userId)
|
||
)`,
|
||
];
|
||
|
||
for (const sql of tables) {
|
||
await this.db.execAsync(sql);
|
||
}
|
||
|
||
// 创建索引
|
||
const indexes = [
|
||
`CREATE INDEX IF NOT EXISTS idx_messages_conversationId ON messages(conversationId)`,
|
||
`CREATE INDEX IF NOT EXISTS idx_messages_seq ON messages(seq)`,
|
||
`CREATE INDEX IF NOT EXISTS idx_messages_conversation_seq ON messages(conversationId, seq)`,
|
||
`CREATE INDEX IF NOT EXISTS idx_group_members_groupId ON group_members(groupId)`,
|
||
];
|
||
|
||
for (const sql of indexes) {
|
||
await this.db.execAsync(sql);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理错误
|
||
*/
|
||
private handleError(error: unknown, operation: string): never {
|
||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||
throw new DataSourceError(
|
||
`Database ${operation} failed: ${message}`,
|
||
'DB_ERROR',
|
||
'LocalDataSource',
|
||
error instanceof Error ? error : undefined
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 检查数据库连接
|
||
*/
|
||
private ensureDb(): SQLite.SQLiteDatabase {
|
||
if (!this.db) {
|
||
throw new DataSourceError(
|
||
'Database not initialized',
|
||
'DB_NOT_INITIALIZED',
|
||
'LocalDataSource'
|
||
);
|
||
}
|
||
return this.db;
|
||
}
|
||
|
||
// ==================== ILocalDataSource 实现 ====================
|
||
|
||
async query<T>(sql: string, params?: any[]): Promise<T[]> {
|
||
try {
|
||
const db = this.ensureDb();
|
||
return await db.getAllAsync<T>(sql, params);
|
||
} catch (error) {
|
||
this.handleError(error, 'QUERY');
|
||
}
|
||
}
|
||
|
||
async getFirst<T>(sql: string, params?: any[]): Promise<T | null> {
|
||
try {
|
||
const db = this.ensureDb();
|
||
return await db.getFirstAsync<T>(sql, params);
|
||
} catch (error) {
|
||
this.handleError(error, 'GET_FIRST');
|
||
}
|
||
}
|
||
|
||
async execute(sql: string, params?: any[]): Promise<void> {
|
||
try {
|
||
const db = this.ensureDb();
|
||
await db.execAsync(sql);
|
||
} catch (error) {
|
||
this.handleError(error, 'EXECUTE');
|
||
}
|
||
}
|
||
|
||
async run(sql: string, params?: any[]): Promise<{ lastInsertRowId: number; changes: number }> {
|
||
try {
|
||
const db = this.ensureDb();
|
||
return await db.runAsync(sql, params);
|
||
} catch (error) {
|
||
this.handleError(error, 'RUN');
|
||
}
|
||
}
|
||
|
||
async transaction(operations: () => Promise<void>): Promise<void> {
|
||
try {
|
||
const db = this.ensureDb();
|
||
await db.withTransactionAsync(async () => {
|
||
await operations();
|
||
});
|
||
} catch (error) {
|
||
this.handleError(error, 'TRANSACTION');
|
||
}
|
||
}
|
||
|
||
async batch(operations: (db: ILocalDataSource) => Promise<void>): Promise<void> {
|
||
try {
|
||
await this.transaction(async () => {
|
||
await operations(this);
|
||
});
|
||
} catch (error) {
|
||
this.handleError(error, 'BATCH');
|
||
}
|
||
}
|
||
|
||
// ==================== 队列写入支持 ====================
|
||
|
||
/**
|
||
* 使用队列执行写入操作(避免并发写入冲突)
|
||
*/
|
||
async enqueueWrite<T>(operation: () => Promise<T>): Promise<T> {
|
||
const wrappedOperation = async () => {
|
||
try {
|
||
return await operation();
|
||
} catch (error) {
|
||
// 尝试重连后重试
|
||
if (this.isRecoverableError(error)) {
|
||
console.error('数据库写入异常,尝试重连后重试:', error);
|
||
this.db = null;
|
||
dbInstance = null;
|
||
await this.initialize();
|
||
return operation();
|
||
}
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
const queued = writeQueue.then(wrappedOperation, wrappedOperation);
|
||
writeQueue = queued.then(() => undefined, () => undefined);
|
||
return queued;
|
||
}
|
||
|
||
/**
|
||
* 判断错误是否可恢复
|
||
*/
|
||
private isRecoverableError(error: unknown): boolean {
|
||
const message = String(error);
|
||
return (
|
||
message.includes('NativeDatabase.prepareAsync') ||
|
||
message.includes('NullPointerException') ||
|
||
message.includes('database is locked')
|
||
);
|
||
}
|
||
}
|
||
|
||
// 导出单例实例
|
||
export const localDataSource = new LocalDataSource();
|