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();
|