Files
frontend/src/data/datasources/LocalDataSource.ts

282 lines
7.5 KiB
TypeScript
Raw Normal View History

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