feat(messaging): implement Outbox persistence with three-tier sync strategy
Add Outbox pattern for reliable message sending with offline recovery: - Persist pending/failed messages to SQLite (status field extended with 'pending'|'failed') - App restart scans pending messages and auto-retries failed sends - Add versioned migration system (PRAGMA user_version) for future schema evolution Implement three-tier sync strategy for precision and efficiency: - syncByVersion: precise incremental sync using persisted cursor (Matrix-style) - syncBySeq: fallback seq-based incremental sync - Full refresh: last resort for recovery Add conversation sync state machine for UI error visibility: - 'idle' | 'hydrating' | 'synced' | 'error' states exposed via useMessages hook - Enables loading/error indicators during hydration pipeline Standardize error handling across message services using createErrorHandler. Add tests: messageOutbox.test.ts, syncByVersion.test.ts, syncState.test.ts
This commit is contained in:
@@ -23,6 +23,42 @@ import {
|
||||
CREATE_POSTS_CACHE_TABLE,
|
||||
} from './PostSchema';
|
||||
|
||||
/**
|
||||
* 迁移版本号管理(对标 expo-sqlite 官方推荐用 PRAGMA user_version)。
|
||||
*
|
||||
* 设计:
|
||||
* - version 0:基线(建表 + 建索引),所有老库都已执行过(靠 CREATE IF NOT EXISTS 幂等)
|
||||
* - version 1:列级迁移(补 seq/status/segments/lastSeq 列),即原 MIGRATE_* 函数
|
||||
*
|
||||
* 向后兼容:老库 user_version 默认为 0,会重新跑 version 1 的列迁移,
|
||||
* 但 MIGRATE_* 靠 PRAGMA table_info 列检查幂等,重复执行无副作用。
|
||||
*
|
||||
* 扩展方式(未来破坏性迁移):在 MIGRATIONS 注册表新增 version 2/3/...,
|
||||
* 每个 step 靠 transaction 包裹保证原子性(失败回滚,不污染 user_version)。
|
||||
*/
|
||||
export const CURRENT_SCHEMA_VERSION = 1;
|
||||
|
||||
/** 读取当前 user_version */
|
||||
async function getUserVersion(db: SQLiteDatabase): Promise<number> {
|
||||
const result = await db.getFirstAsync<{ user_version: number }>('PRAGMA user_version');
|
||||
return result?.user_version ?? 0;
|
||||
}
|
||||
|
||||
/** 设置 user_version */
|
||||
async function setUserVersion(db: SQLiteDatabase, version: number): Promise<void> {
|
||||
await execWithRetry(db, `PRAGMA user_version = ${version};`);
|
||||
}
|
||||
|
||||
/** 版本化迁移注册表:key = 目标版本,value = 该版本要执行的迁移逻辑 */
|
||||
const MIGRATIONS: Map<number, (db: SQLiteDatabase) => Promise<void>> = new Map([
|
||||
// version 1:列级迁移(补 seq/status/segments/lastSeq 列)
|
||||
// 历史背景:最初表结构不含这些列,靠 MIGRATE_* 增量 ALTER。现在纳入版本管理。
|
||||
[1, async (db: SQLiteDatabase) => {
|
||||
await MIGRATE_MESSAGES_TABLE(db);
|
||||
await MIGRATE_CONVERSATIONS_TABLE(db);
|
||||
}],
|
||||
]);
|
||||
|
||||
async function execWithRetry(db: SQLiteDatabase, sql: string, maxRetries = 5): Promise<void> {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
@@ -41,9 +77,19 @@ async function execWithRetry(db: SQLiteDatabase, sql: string, maxRetries = 5): P
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行数据库迁移。
|
||||
*
|
||||
* 流程:
|
||||
* 1. 设置 busy_timeout
|
||||
* 2. 基线建表 + 建索引(version 0,CREATE IF NOT EXISTS 幂等,每次都执行以保证新表存在)
|
||||
* 3. 按 user_version 顺序执行版本化迁移,每步成功后递增 user_version
|
||||
*/
|
||||
export async function runMigrations(db: SQLiteDatabase): Promise<void> {
|
||||
await execWithRetry(db, 'PRAGMA busy_timeout = 10000;');
|
||||
|
||||
|
||||
// ---- 基线:version 0(建表 + 建索引)----
|
||||
// 保留每次执行,确保新库/损坏恢复时表结构就位;对老库靠 IF NOT EXISTS 幂等。
|
||||
await execWithRetry(db, CREATE_MESSAGES_TABLE);
|
||||
await execWithRetry(db, CREATE_CONVERSATIONS_TABLE);
|
||||
await execWithRetry(db, CREATE_CONVERSATION_CACHE_TABLE);
|
||||
@@ -61,6 +107,32 @@ export async function runMigrations(db: SQLiteDatabase): Promise<void> {
|
||||
await execWithRetry(db, indexSql);
|
||||
}
|
||||
|
||||
await MIGRATE_MESSAGES_TABLE(db);
|
||||
await MIGRATE_CONVERSATIONS_TABLE(db);
|
||||
}
|
||||
// ---- 版本化迁移:version 1 ~ CURRENT_SCHEMA_VERSION ----
|
||||
const currentVersion = await getUserVersion(db);
|
||||
|
||||
// 当前版本已是最新(含未来更高版本),跳过
|
||||
if (currentVersion >= CURRENT_SCHEMA_VERSION) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 从 currentVersion+1 开始,逐版本执行迁移
|
||||
for (let target = currentVersion + 1; target <= CURRENT_SCHEMA_VERSION; target++) {
|
||||
const migration = MIGRATIONS.get(target);
|
||||
if (!migration) {
|
||||
console.warn(`[MigrationManager] 未找到 version ${target} 的迁移逻辑,跳过`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await migration(db);
|
||||
// 每个版本迁移成功后立即持久化 user_version,
|
||||
// 即使后续版本失败,已完成的版本不会重复执行
|
||||
await setUserVersion(db, target);
|
||||
} catch (error) {
|
||||
console.error(`[MigrationManager] 迁移到 version ${target} 失败:`, error);
|
||||
// 不抛出致命错误以避免阻塞 App 启动;降级运行(表可能缺列,MIGRATE_* 内部会兜底)
|
||||
// 但记录版本未推进,下次启动会重试该版本
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user