refactor(database): migrate to new modular database layer and unify data access
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 2m44s
Frontend CI / ota-android (push) Successful in 12m51s
Frontend CI / build-android-apk (push) Successful in 1h1m26s

- 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:
lafay
2026-04-04 08:01:45 +08:00
parent 189b977fac
commit 82c2970a85
76 changed files with 3382 additions and 2000 deletions

View File

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

View File

@@ -5,5 +5,5 @@
export * from './interfaces';
export * from './ApiDataSource';
export * from './LocalDataSource';
export { LocalDataSource, localDataSource } from '@/database';
export * from './CacheDataSource';

View File

@@ -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>;
}
// 缓存数据源接口