From def3259178bceb9662d6e2cbe934499c0d7696f4 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sun, 18 Jan 2026 18:20:22 +0000 Subject: [PATCH] Handle Flask sync failures with retries --- src/store/middleware/flaskSync.ts | 1 + src/store/middleware/persistenceMiddleware.ts | 117 +++++++++++++++++- 2 files changed, 112 insertions(+), 6 deletions(-) diff --git a/src/store/middleware/flaskSync.ts b/src/store/middleware/flaskSync.ts index 99a705d..c7c7cbc 100644 --- a/src/store/middleware/flaskSync.ts +++ b/src/store/middleware/flaskSync.ts @@ -37,6 +37,7 @@ export async function syncToFlask( } } catch (error) { console.error('[FlaskSync] Error syncing to Flask:', error) + throw error } } diff --git a/src/store/middleware/persistenceMiddleware.ts b/src/store/middleware/persistenceMiddleware.ts index 7d20c2e..e429ca1 100644 --- a/src/store/middleware/persistenceMiddleware.ts +++ b/src/store/middleware/persistenceMiddleware.ts @@ -38,10 +38,22 @@ type PendingOperation = { timestamp: number } +type FailedSyncOperation = PendingOperation & { + attempt: number + lastError: string + nextRetryAt: number +} + +const MAX_SYNC_RETRIES = 5 +const BASE_SYNC_RETRY_DELAY_MS = 1000 +const MAX_SYNC_RETRY_DELAY_MS = 30000 + class PersistenceQueue { private queue: Map = new Map() private processing = false private debounceTimers: Map> = new Map() + private failedSyncs: Map = new Map() + private retryTimers: Map> = new Map() enqueue(operation: PendingOperation, debounceMs: number) { const opKey = `${operation.storeName}:${operation.key}` @@ -75,14 +87,10 @@ class PersistenceQueue { try { if (op.type === 'put') { await db.put(op.storeName as any, op.value) - if (sliceToPersistenceMap[op.storeName]?.syncToFlask) { - await syncToFlask(op.storeName, op.key, op.value, 'put') - } + await this.syncToFlaskWithRetry(op, op.value) } else if (op.type === 'delete') { await db.delete(op.storeName as any, op.key) - if (sliceToPersistenceMap[op.storeName]?.syncToFlask) { - await syncToFlask(op.storeName, op.key, null, 'delete') - } + await this.syncToFlaskWithRetry(op, null) } } catch (error) { console.error(`[PersistenceMiddleware] Failed to persist ${op.type} for ${op.storeName}:${op.key}`, error) @@ -100,6 +108,18 @@ class PersistenceQueue { } } + getFailedSyncs() { + return Array.from(this.failedSyncs.values()).sort((a, b) => a.nextRetryAt - b.nextRetryAt) + } + + async retryFailedSyncs() { + for (const [opKey, failure] of this.failedSyncs.entries()) { + if (failure.nextRetryAt <= Date.now()) { + await this.retryFailedSync(opKey) + } + } + } + async flush() { for (const timer of this.debounceTimers.values()) { clearTimeout(timer) @@ -107,6 +127,89 @@ class PersistenceQueue { this.debounceTimers.clear() await this.processQueue() } + + private async syncToFlaskWithRetry(op: PendingOperation, value: any) { + if (!sliceToPersistenceMap[op.storeName]?.syncToFlask) return + + try { + await syncToFlask(op.storeName, op.key, value, op.type) + this.clearSyncFailure(op) + } catch (error) { + this.recordSyncFailure(op, error) + console.warn( + `[PersistenceMiddleware] Flask sync failed for ${op.storeName}:${op.key} (${op.type}); queued for retry.`, + error + ) + } + } + + private recordSyncFailure(op: PendingOperation, error: unknown) { + const opKey = this.getFailureKey(op) + const previous = this.failedSyncs.get(opKey) + const attempt = previous ? previous.attempt + 1 : 1 + const delayMs = this.getRetryDelayMs(attempt) + const nextRetryAt = Date.now() + delayMs + const lastError = error instanceof Error ? error.message : String(error) + + this.failedSyncs.set(opKey, { + ...op, + attempt, + lastError, + nextRetryAt, + }) + + const existingTimer = this.retryTimers.get(opKey) + if (existingTimer) { + clearTimeout(existingTimer) + } + + if (attempt <= MAX_SYNC_RETRIES) { + const timer = setTimeout(() => { + this.retryTimers.delete(opKey) + void this.retryFailedSync(opKey) + }, delayMs) + this.retryTimers.set(opKey, timer) + } + } + + private clearSyncFailure(op: PendingOperation) { + const opKey = this.getFailureKey(op) + const timer = this.retryTimers.get(opKey) + if (timer) { + clearTimeout(timer) + this.retryTimers.delete(opKey) + } + this.failedSyncs.delete(opKey) + } + + private async retryFailedSync(opKey: string) { + const failure = this.failedSyncs.get(opKey) + if (!failure) return + + if (failure.attempt > MAX_SYNC_RETRIES) { + return + } + + this.enqueue( + { + type: failure.type, + storeName: failure.storeName, + key: failure.key, + value: failure.value, + timestamp: Date.now(), + }, + 0 + ) + } + + private getRetryDelayMs(attempt: number) { + const delay = BASE_SYNC_RETRY_DELAY_MS * Math.pow(2, attempt - 1) + return Math.min(delay, MAX_SYNC_RETRY_DELAY_MS) + } + + private getFailureKey(op: PendingOperation) { + return `${op.storeName}:${op.key}:${op.type}` + } } const persistenceQueue = new PersistenceQueue() @@ -208,6 +311,8 @@ export const createPersistenceMiddleware = (): Middleware => { } export const flushPersistence = () => persistenceQueue.flush() +export const getFailedSyncOperations = () => persistenceQueue.getFailedSyncs() +export const retryFailedSyncOperations = () => persistenceQueue.retryFailedSyncs() export const configurePersistence = (sliceName: string, config: Partial) => { if (sliceToPersistenceMap[sliceName]) {