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:
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
import { showPrompt } from '../ui/promptService';
|
||||
import { ApiError, isNetworkError } from './api';
|
||||
|
||||
export enum AppErrorCode {
|
||||
NETWORK_ERROR = 'NETWORK_ERROR',
|
||||
@@ -45,6 +46,19 @@ const ERROR_MESSAGES: Record<AppErrorCode, string> = {
|
||||
function classifyError(error: unknown): AppErrorCode {
|
||||
if (!error) return AppErrorCode.UNKNOWN;
|
||||
|
||||
// 优先识别项目自有 ApiError(含 errorCode 字段,比 message 关键词匹配更可靠)
|
||||
// 修复衔接缺口:api.ts 用 status=503 + errorCode='NETWORK_ERROR' 表示网络错误,
|
||||
// 但原逻辑只看 status≥500 会误判为 SERVER_ERROR。这里用 isNetworkError 精确识别。
|
||||
if (isNetworkError(error)) return AppErrorCode.NETWORK_ERROR;
|
||||
if (error instanceof ApiError) {
|
||||
if (error.errorCode === 'AUTH_ERROR' || error.code === 401) return AppErrorCode.AUTH_ERROR;
|
||||
if (error.errorCode === 'FORBIDDEN' || error.code === 403) return AppErrorCode.FORBIDDEN;
|
||||
if (error.errorCode === 'NOT_FOUND' || error.code === 404) return AppErrorCode.NOT_FOUND;
|
||||
if (error.errorCode === 'VERIFICATION_REQUIRED') return AppErrorCode.VALIDATION_ERROR;
|
||||
if (error.code === 400 || error.code === 422) return AppErrorCode.VALIDATION_ERROR;
|
||||
if (typeof error.code === 'number' && error.code >= 500) return AppErrorCode.SERVER_ERROR;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
const message = error.message.toLowerCase();
|
||||
|
||||
|
||||
@@ -304,6 +304,11 @@ class WebSocketService {
|
||||
preventDisconnectOnBackground = false;
|
||||
|
||||
private getWSUrl(token: string | null): string {
|
||||
// 当前仅携带 token + compress。
|
||||
// 待后端支持 event 级 resume 后,可在此拼接 last_event_id 实现断点续传:
|
||||
// const eventIdParam = this.lastEventId ? `&last_event_id=${encodeURIComponent(this.lastEventId)}` : '';
|
||||
// return `${WS_URL}?token=${encodeURIComponent(token || '')}&compress=1${eventIdParam}`;
|
||||
// 届时重连后服务端从 lastEventId 续传断线期间的事件,无需全量 re-sync。
|
||||
return `${WS_URL}?token=${encodeURIComponent(token || '')}&compress=1`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user