feat(messaging): implement Outbox persistence with three-tier sync strategy
Some checks failed
Frontend CI / build-android-apk (push) Waiting to run
Frontend CI / ota (android) (push) Successful in 1m52s
Frontend CI / ota (ios) (push) Successful in 2m36s
Frontend CI / build-and-push-web (push) Failing after 1m15s

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:
lafay
2026-06-28 23:19:52 +08:00
parent d8386b5f76
commit f9b0999112
23 changed files with 1094 additions and 85 deletions

View File

@@ -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 0CREATE 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;
}
}
}