refactor(database): migrate to new modular database layer and unify data access
- 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/
This commit is contained in:
@@ -1,218 +0,0 @@
|
||||
/**
|
||||
* 本地数据库数据源实现
|
||||
* 复用 database.ts 的共享连接,避免 OPFS 句柄冲突
|
||||
*/
|
||||
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { ILocalDataSource, DataSourceError } from './interfaces';
|
||||
import {
|
||||
initDatabase,
|
||||
getCurrentDbUserId,
|
||||
isDbInitialized,
|
||||
getDbInstance,
|
||||
} from '../../services/database';
|
||||
|
||||
let writeQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
export interface LocalDataSourceConfig {
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export class LocalDataSource implements ILocalDataSource {
|
||||
private userId: string | null = null;
|
||||
private initialized = false;
|
||||
private initializingPromise: Promise<void> | null = null;
|
||||
|
||||
constructor(config: LocalDataSourceConfig = {}) {
|
||||
this.userId = config.userId || null;
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized && isDbInitialized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.initializingPromise) {
|
||||
await this.initializingPromise;
|
||||
return;
|
||||
}
|
||||
|
||||
this.initializingPromise = this.doInitialize();
|
||||
try {
|
||||
await this.initializingPromise;
|
||||
} finally {
|
||||
this.initializingPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async doInitialize(): Promise<void> {
|
||||
try {
|
||||
// 如果 database.ts 已经初始化,复用它的连接
|
||||
if (isDbInitialized()) {
|
||||
const currentUserId = getCurrentDbUserId();
|
||||
if (this.userId && this.userId !== currentUserId) {
|
||||
await initDatabase(this.userId);
|
||||
}
|
||||
} else if (this.userId) {
|
||||
await initDatabase(this.userId);
|
||||
} else {
|
||||
const currentUserId = getCurrentDbUserId();
|
||||
if (!currentUserId) {
|
||||
console.warn('[LocalDataSource] 没有用户登录,跳过初始化');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const db = getDbInstance();
|
||||
if (db) {
|
||||
await db.execAsync(`
|
||||
CREATE TABLE IF NOT EXISTS posts_cache (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
this.handleError(error, 'INITIALIZE');
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
const db = getDbInstance();
|
||||
if (!db) {
|
||||
throw new DataSourceError(
|
||||
'Database not initialized',
|
||||
'DB_NOT_INITIALIZED',
|
||||
'LocalDataSource'
|
||||
);
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
async query<T>(sql: string, params?: any[]): Promise<T[]> {
|
||||
try {
|
||||
await this.initialize();
|
||||
const db = this.ensureDb();
|
||||
if (params && params.length > 0) {
|
||||
return await db.getAllAsync<T>(sql, params as any);
|
||||
}
|
||||
return await db.getAllAsync<T>(sql);
|
||||
} catch (error) {
|
||||
this.handleError(error, 'QUERY');
|
||||
}
|
||||
}
|
||||
|
||||
async getFirst<T>(sql: string, params?: any[]): Promise<T | null> {
|
||||
try {
|
||||
await this.initialize();
|
||||
const db = this.ensureDb();
|
||||
if (params && params.length > 0) {
|
||||
return await db.getFirstAsync<T>(sql, params as any);
|
||||
}
|
||||
return await db.getFirstAsync<T>(sql);
|
||||
} catch (error) {
|
||||
this.handleError(error, 'GET_FIRST');
|
||||
}
|
||||
}
|
||||
|
||||
async execute(sql: string, params?: any[]): Promise<void> {
|
||||
try {
|
||||
await this.initialize();
|
||||
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 {
|
||||
await this.initialize();
|
||||
const db = this.ensureDb();
|
||||
if (params && params.length > 0) {
|
||||
return await db.runAsync(sql, params as any);
|
||||
}
|
||||
return await db.runAsync(sql);
|
||||
} catch (error) {
|
||||
if (this.isRecoverableError(error)) {
|
||||
this.initialized = false;
|
||||
await this.initialize();
|
||||
const db = this.ensureDb();
|
||||
if (params && params.length > 0) {
|
||||
return await db.runAsync(sql, params as any);
|
||||
}
|
||||
return await db.runAsync(sql);
|
||||
}
|
||||
this.handleError(error, 'RUN');
|
||||
}
|
||||
}
|
||||
|
||||
async transaction(operations: () => Promise<void>): Promise<void> {
|
||||
try {
|
||||
await this.initialize();
|
||||
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> {
|
||||
await this.initialize();
|
||||
const wrappedOperation = async () => {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
if (this.isRecoverableError(error)) {
|
||||
console.error('[LocalDataSource] 数据库写入异常,尝试重连后重试:', error);
|
||||
this.initialized = false;
|
||||
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') ||
|
||||
message.includes('finalizing statement') ||
|
||||
message.includes('cannot create file')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const localDataSource = new LocalDataSource();
|
||||
@@ -5,5 +5,5 @@
|
||||
|
||||
export * from './interfaces';
|
||||
export * from './ApiDataSource';
|
||||
export * from './LocalDataSource';
|
||||
export { LocalDataSource, localDataSource } from '@/database';
|
||||
export * from './CacheDataSource';
|
||||
|
||||
@@ -20,6 +20,8 @@ export interface ILocalDataSource {
|
||||
run(sql: string, params?: any[]): Promise<{ lastInsertRowId: number; changes: number }>;
|
||||
transaction(operations: () => Promise<void>): Promise<void>;
|
||||
batch(operations: (db: ILocalDataSource) => Promise<void>): Promise<void>;
|
||||
enqueueWrite<T>(operation: () => Promise<T>): Promise<T>;
|
||||
initialize(userId?: string): Promise<void>;
|
||||
}
|
||||
|
||||
// 缓存数据源接口
|
||||
|
||||
Reference in New Issue
Block a user