- Remove legacy database.ts, LocalDataSource.ts, and MessageRepository.ts - Create new src/database/ module with messageRepository, userCacheRepository, conversationRepository, and groupCacheRepository - Update all consumers to import from @/database instead of services/database - Add web platform blur handling for modal components to fix focus issues - Flatten SystemMessageItem and NotificationsScreen styles for consistent design - Add draggable slider in ChatSettingsScreen and dynamic font size support - Introduce 9 new chat color themes - Add profile screens for about, terms, and privacy policy with navigation routes - Add policy links to login and registration screens - Fix post share URL format from /posts/ to /post/
196 lines
6.4 KiB
TypeScript
196 lines
6.4 KiB
TypeScript
import * as SQLite from 'expo-sqlite';
|
|
import { LockManager } from './LockManager';
|
|
import { DB_CONFIG, getDbName } from './DatabaseConfig';
|
|
import { runMigrations } from '../schema/MigrationManager';
|
|
|
|
type SQLiteDatabase = SQLite.SQLiteDatabase;
|
|
|
|
class DatabaseManager {
|
|
private static instance: DatabaseManager;
|
|
private db: SQLiteDatabase | null = null;
|
|
private currentUserId: string | null = null;
|
|
private initPromise: Promise<void> | null = null;
|
|
private initTargetUserId: string | null = null;
|
|
private isInitialized = false;
|
|
|
|
static getInstance(): DatabaseManager {
|
|
if (!DatabaseManager.instance) {
|
|
DatabaseManager.instance = new DatabaseManager();
|
|
}
|
|
return DatabaseManager.instance;
|
|
}
|
|
|
|
async initialize(userId?: string | null): Promise<void> {
|
|
const targetUserId = userId || null;
|
|
const callId = Date.now();
|
|
|
|
console.log(`[DatabaseManager ${callId}] initialize 开始, userId=${targetUserId}, 当前db=${!!this.db}, 当前userId=${this.currentUserId}`);
|
|
|
|
if (this.db && this.currentUserId === targetUserId) {
|
|
console.log(`[DatabaseManager ${callId}] 已是目标数据库,直接返回`);
|
|
this.isInitialized = true;
|
|
return;
|
|
}
|
|
|
|
if (this.initPromise && this.initTargetUserId === targetUserId) {
|
|
console.log(`[DatabaseManager ${callId}] 等待进行中的初始化`);
|
|
await this.initPromise;
|
|
console.log(`[DatabaseManager ${callId}] 初始化完成`);
|
|
return;
|
|
}
|
|
|
|
if (this.initPromise) {
|
|
console.log(`[DatabaseManager ${callId}] 等待其他初始化完成`);
|
|
await this.initPromise.catch(() => {});
|
|
}
|
|
|
|
if (this.db && this.currentUserId === targetUserId) {
|
|
console.log(`[DatabaseManager ${callId}] 其他初始化已完成目标,直接返回`);
|
|
this.isInitialized = true;
|
|
return;
|
|
}
|
|
|
|
this.initTargetUserId = targetUserId;
|
|
console.log(`[DatabaseManager ${callId}] 开始实际初始化`);
|
|
|
|
this.initPromise = LockManager.withMutex(async () => {
|
|
const mutexId = callId;
|
|
console.log(`[DatabaseManager ${mutexId}] 获得互斥锁`);
|
|
|
|
if (this.db && this.currentUserId === targetUserId) {
|
|
console.log(`[DatabaseManager ${mutexId}] 互斥锁内检查:已是目标数据库`);
|
|
this.isInitialized = true;
|
|
return;
|
|
}
|
|
|
|
const isWeb = typeof window !== 'undefined';
|
|
|
|
if (this.db) {
|
|
console.log(`[DatabaseManager ${mutexId}] 关闭旧数据库连接...`);
|
|
try {
|
|
await this.db.closeAsync();
|
|
console.log(`[DatabaseManager ${mutexId}] 旧连接已关闭`);
|
|
} catch (e) {
|
|
console.warn(`[DatabaseManager ${mutexId}] 关闭旧连接失败:`, e);
|
|
}
|
|
this.db = null;
|
|
this.currentUserId = null;
|
|
this.isInitialized = false;
|
|
|
|
if (isWeb) {
|
|
await new Promise(r => setTimeout(r, DB_CONFIG.WEB_CLOSE_DELAY));
|
|
}
|
|
}
|
|
|
|
const dbName = getDbName(targetUserId);
|
|
console.log(`[DatabaseManager ${mutexId}] 打开数据库: ${dbName}`);
|
|
|
|
const database = await this.openDatabaseWithRetry(dbName);
|
|
console.log(`[DatabaseManager ${mutexId}] 数据库已打开,执行迁移`);
|
|
|
|
await runMigrations(database);
|
|
console.log(`[DatabaseManager ${mutexId}] 迁移完成`);
|
|
|
|
this.db = database;
|
|
this.currentUserId = targetUserId;
|
|
this.isInitialized = true;
|
|
console.log(`[DatabaseManager ${mutexId}] 初始化完成`);
|
|
}).finally(() => {
|
|
console.log(`[DatabaseManager ${callId}] 清理初始化状态`);
|
|
this.initPromise = null;
|
|
this.initTargetUserId = null;
|
|
});
|
|
|
|
await this.initPromise;
|
|
console.log(`[DatabaseManager ${callId}] initialize 返回`);
|
|
}
|
|
|
|
private async openDatabaseWithRetry(dbName: string): Promise<SQLiteDatabase> {
|
|
const isWeb = typeof window !== 'undefined';
|
|
|
|
return LockManager.withWebLock(`${DB_CONFIG.LOCK_PREFIX}${dbName}`, async () => {
|
|
for (let attempt = 0; attempt <= DB_CONFIG.MAX_RETRIES; attempt++) {
|
|
try {
|
|
const database = await SQLite.openDatabaseAsync(dbName);
|
|
console.log(`[DatabaseManager] 成功打开数据库 (尝试 ${attempt + 1}/${DB_CONFIG.MAX_RETRIES + 1})`);
|
|
return database;
|
|
} catch (error) {
|
|
const msg = String(error);
|
|
console.error(`[DatabaseManager] 打开失败 (尝试 ${attempt + 1}/${DB_CONFIG.MAX_RETRIES + 1}):`, msg);
|
|
|
|
if (attempt < DB_CONFIG.MAX_RETRIES) {
|
|
const delay = isWeb ? DB_CONFIG.WEB_RETRY_DELAY_BASE * (attempt + 1) : DB_CONFIG.NATIVE_RETRY_DELAY;
|
|
console.warn(`[DatabaseManager] ${delay}ms 后重试...`);
|
|
await new Promise(r => setTimeout(r, delay));
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
throw new Error('打开数据库失败');
|
|
});
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
if (this.initPromise) {
|
|
await this.initPromise.catch(() => {});
|
|
}
|
|
|
|
const dbName = getDbName(this.currentUserId);
|
|
|
|
await LockManager.withWebLock(`${DB_CONFIG.LOCK_PREFIX}${dbName}`, async () => {
|
|
await LockManager.withMutex(async () => {
|
|
if (!this.db) return;
|
|
|
|
try {
|
|
await this.db.closeAsync();
|
|
console.log('[DatabaseManager] 数据库已关闭');
|
|
|
|
if (typeof window !== 'undefined') {
|
|
await new Promise(r => setTimeout(r, DB_CONFIG.WEB_FINAL_CLOSE_DELAY));
|
|
}
|
|
} catch (e) {
|
|
console.warn('[DatabaseManager] 关闭失败:', e);
|
|
}
|
|
this.db = null;
|
|
this.currentUserId = null;
|
|
this.isInitialized = false;
|
|
});
|
|
});
|
|
}
|
|
|
|
async getDb(timeout: number = 10000): Promise<SQLiteDatabase> {
|
|
if (this.isInitialized && this.db) return this.db;
|
|
|
|
if (this.initPromise) {
|
|
const timeoutPromise = new Promise<null>((_, reject) =>
|
|
setTimeout(() => reject(new Error('等待数据库初始化超时')), timeout)
|
|
);
|
|
|
|
await Promise.race([this.initPromise, timeoutPromise]);
|
|
|
|
if (this.db) return this.db;
|
|
}
|
|
|
|
throw new Error('数据库未初始化,请先调用 initialize(userId)');
|
|
}
|
|
|
|
getDbSync(): SQLiteDatabase | null {
|
|
return this.db;
|
|
}
|
|
|
|
getCurrentUserId(): string | null {
|
|
return this.currentUserId;
|
|
}
|
|
|
|
isReady(): boolean {
|
|
return this.isInitialized && this.db !== null;
|
|
}
|
|
|
|
async withMutex<T>(fn: () => Promise<T>): Promise<T> {
|
|
return LockManager.withMutex(fn);
|
|
}
|
|
}
|
|
|
|
export const databaseManager = DatabaseManager.getInstance();
|
|
export { DatabaseManager }; |