From 595f1ae9c0eb07bd4018c5dd28e71a9474340f48 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sat, 17 Jan 2026 18:57:01 +0000 Subject: [PATCH] Generated by Spark: A library in packages folder - Seems its still trying to use fetch. If a fetch fails, switch back to IndexedDB. --- src/lib/storage-adapter.ts | 73 ++++++++++++++++++++++++----- src/lib/unified-storage.ts | 96 ++++++++++++++++++++++++++------------ 2 files changed, 127 insertions(+), 42 deletions(-) diff --git a/src/lib/storage-adapter.ts b/src/lib/storage-adapter.ts index 20159fa..33031fa 100644 --- a/src/lib/storage-adapter.ts +++ b/src/lib/storage-adapter.ts @@ -16,21 +16,41 @@ export interface StorageAdapter { class FlaskBackendAdapter implements StorageAdapter { private baseUrl: string private isAvailable: boolean | null = null + private readonly TIMEOUT_MS = 2000 constructor(baseUrl: string) { this.baseUrl = baseUrl.replace(/\/$/, '') } + private async fetchWithTimeout(url: string, options: RequestInit = {}): Promise { + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), this.TIMEOUT_MS) + + try { + const response = await fetch(url, { + ...options, + signal: controller.signal, + }) + clearTimeout(timeoutId) + return response + } catch (error: any) { + clearTimeout(timeoutId) + if (error.name === 'AbortError') { + throw new Error(`Request timeout after ${this.TIMEOUT_MS}ms`) + } + throw error + } + } + private async checkAvailability(): Promise { if (this.isAvailable !== null) { return this.isAvailable } try { - const response = await fetch(`${this.baseUrl}/health`, { + const response = await this.fetchWithTimeout(`${this.baseUrl}/health`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, - signal: AbortSignal.timeout(3000), }) this.isAvailable = response.ok console.log('[StorageAdapter] Flask backend available:', this.isAvailable) @@ -48,7 +68,7 @@ class FlaskBackendAdapter implements StorageAdapter { } try { - const response = await fetch(`${this.baseUrl}/api/storage/${encodeURIComponent(key)}`, { + const response = await this.fetchWithTimeout(`${this.baseUrl}/api/storage/${encodeURIComponent(key)}`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, }) @@ -65,6 +85,7 @@ class FlaskBackendAdapter implements StorageAdapter { return data.value as T } catch (error) { console.error(`[StorageAdapter] Error getting key ${key}:`, error) + this.isAvailable = false throw error } } @@ -75,7 +96,7 @@ class FlaskBackendAdapter implements StorageAdapter { } try { - const response = await fetch(`${this.baseUrl}/api/storage/${encodeURIComponent(key)}`, { + const response = await this.fetchWithTimeout(`${this.baseUrl}/api/storage/${encodeURIComponent(key)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value }), @@ -86,6 +107,7 @@ class FlaskBackendAdapter implements StorageAdapter { } } catch (error) { console.error(`[StorageAdapter] Error setting key ${key}:`, error) + this.isAvailable = false throw error } } @@ -96,7 +118,7 @@ class FlaskBackendAdapter implements StorageAdapter { } try { - const response = await fetch(`${this.baseUrl}/api/storage/${encodeURIComponent(key)}`, { + const response = await this.fetchWithTimeout(`${this.baseUrl}/api/storage/${encodeURIComponent(key)}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, }) @@ -110,6 +132,7 @@ class FlaskBackendAdapter implements StorageAdapter { } } catch (error) { console.error(`[StorageAdapter] Error deleting key ${key}:`, error) + this.isAvailable = false throw error } } @@ -120,7 +143,7 @@ class FlaskBackendAdapter implements StorageAdapter { } try { - const response = await fetch(`${this.baseUrl}/api/storage/keys`, { + const response = await this.fetchWithTimeout(`${this.baseUrl}/api/storage/keys`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, }) @@ -133,6 +156,7 @@ class FlaskBackendAdapter implements StorageAdapter { return data.keys } catch (error) { console.error('[StorageAdapter] Error getting keys:', error) + this.isAvailable = false throw error } } @@ -143,7 +167,7 @@ class FlaskBackendAdapter implements StorageAdapter { } try { - const response = await fetch(`${this.baseUrl}/api/storage/clear`, { + const response = await this.fetchWithTimeout(`${this.baseUrl}/api/storage/clear`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, }) @@ -153,6 +177,7 @@ class FlaskBackendAdapter implements StorageAdapter { } } catch (error) { console.error('[StorageAdapter] Error clearing storage:', error) + this.isAvailable = false throw error } } @@ -163,7 +188,7 @@ class FlaskBackendAdapter implements StorageAdapter { } try { - const response = await fetch(`${this.baseUrl}/api/storage/export`, { + const response = await this.fetchWithTimeout(`${this.baseUrl}/api/storage/export`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, }) @@ -175,6 +200,7 @@ class FlaskBackendAdapter implements StorageAdapter { return await response.json() } catch (error) { console.error('[StorageAdapter] Error exporting data:', error) + this.isAvailable = false throw error } } @@ -185,7 +211,7 @@ class FlaskBackendAdapter implements StorageAdapter { } try { - const response = await fetch(`${this.baseUrl}/api/storage/import`, { + const response = await this.fetchWithTimeout(`${this.baseUrl}/api/storage/import`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), @@ -199,6 +225,7 @@ class FlaskBackendAdapter implements StorageAdapter { return result.imported } catch (error) { console.error('[StorageAdapter] Error importing data:', error) + this.isAvailable = false throw error } } @@ -209,7 +236,7 @@ class FlaskBackendAdapter implements StorageAdapter { } try { - const response = await fetch(`${this.baseUrl}/api/storage/stats`, { + const response = await this.fetchWithTimeout(`${this.baseUrl}/api/storage/stats`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, }) @@ -221,6 +248,7 @@ class FlaskBackendAdapter implements StorageAdapter { return await response.json() } catch (error) { console.error('[StorageAdapter] Error getting stats:', error) + this.isAvailable = false throw error } } @@ -338,6 +366,8 @@ class AutoStorageAdapter implements StorageAdapter { private backendType: 'flask' | 'indexeddb' | null = null private initPromise: Promise | null = null private hasWarnedAboutFallback = false + private failureCount = 0 + private readonly MAX_FAILURES_BEFORE_SWITCH = 3 private async initialize(): Promise { if (this.adapter) { @@ -362,18 +392,39 @@ class AutoStorageAdapter implements StorageAdapter { await this.initPromise } + private switchToFallback(): void { + if (this.backendType === 'flask' && this.fallbackAdapter) { + console.warn('[StorageAdapter] Too many Flask failures detected, permanently switching to IndexedDB for this session') + this.adapter = this.fallbackAdapter + this.backendType = 'indexeddb' + this.fallbackAdapter = null + this.failureCount = 0 + } + } + private async executeWithFallback( operation: () => Promise, fallbackOperation?: () => Promise ): Promise { try { - return await operation() + const result = await operation() + if (this.backendType === 'flask') { + this.failureCount = 0 + } + return result } catch (error) { if (this.backendType === 'flask' && this.fallbackAdapter && fallbackOperation) { + this.failureCount++ + if (!this.hasWarnedAboutFallback) { console.warn('[StorageAdapter] Flask backend operation failed, falling back to IndexedDB:', error) this.hasWarnedAboutFallback = true } + + if (this.failureCount >= this.MAX_FAILURES_BEFORE_SWITCH) { + this.switchToFallback() + } + try { return await fallbackOperation() } catch (fallbackError) { diff --git a/src/lib/unified-storage.ts b/src/lib/unified-storage.ts index ff6d103..611e50c 100644 --- a/src/lib/unified-storage.ts +++ b/src/lib/unified-storage.ts @@ -11,26 +11,41 @@ export interface StorageAdapter { class FlaskBackendAdapter implements StorageAdapter { private baseUrl: string + private readonly TIMEOUT_MS = 2000 constructor(baseUrl?: string) { this.baseUrl = baseUrl || localStorage.getItem('codeforge-flask-url') || import.meta.env.VITE_FLASK_BACKEND_URL || 'http://localhost:5001' } private async request(endpoint: string, options?: RequestInit): Promise { - const response = await fetch(`${this.baseUrl}${endpoint}`, { - ...options, - headers: { - 'Content-Type': 'application/json', - ...options?.headers, - }, - }) + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), this.TIMEOUT_MS) - if (!response.ok) { - const error = await response.json().catch(() => ({ error: response.statusText })) - throw new Error(error.error || `HTTP ${response.status}`) + try { + const response = await fetch(`${this.baseUrl}${endpoint}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + signal: controller.signal, + }) + + clearTimeout(timeoutId) + + if (!response.ok) { + const error = await response.json().catch(() => ({ error: response.statusText })) + throw new Error(error.error || `HTTP ${response.status}`) + } + + return response.json() + } catch (error: any) { + clearTimeout(timeoutId) + if (error.name === 'AbortError') { + throw new Error(`Request timeout after ${this.TIMEOUT_MS}ms`) + } + throw error } - - return response.json() } async get(key: string): Promise { @@ -343,20 +358,6 @@ class UnifiedStorage { const flaskEnvUrl = import.meta.env.VITE_FLASK_BACKEND_URL const preferSQLite = localStorage.getItem('codeforge-prefer-sqlite') === 'true' - if (preferFlask || flaskEnvUrl) { - try { - console.log('[Storage] Flask backend explicitly configured, attempting to initialize...') - const flaskAdapter = new FlaskBackendAdapter(flaskEnvUrl) - await flaskAdapter.get('_health_check') - this.adapter = flaskAdapter - this.backend = 'flask' - console.log('[Storage] ✓ Using Flask backend') - return - } catch (error) { - console.warn('[Storage] Flask backend not available, falling back to IndexedDB:', error) - } - } - if (typeof indexedDB !== 'undefined') { try { console.log('[Storage] Initializing default IndexedDB backend...') @@ -371,6 +372,26 @@ class UnifiedStorage { } } + if (preferFlask || flaskEnvUrl) { + try { + console.log('[Storage] Flask backend explicitly configured, attempting to initialize...') + const flaskAdapter = new FlaskBackendAdapter(flaskEnvUrl) + const testResponse = await Promise.race([ + flaskAdapter.get('_health_check'), + new Promise((_, reject) => setTimeout(() => reject(new Error('Flask connection timeout')), 2000)) + ]) + this.adapter = flaskAdapter + this.backend = 'flask' + console.log('[Storage] ✓ Using Flask backend') + return + } catch (error) { + console.warn('[Storage] Flask backend not available, already using IndexedDB:', error) + if (this.adapter && this.backend === 'indexeddb') { + return + } + } + } + if (preferSQLite) { try { console.log('[Storage] SQLite fallback, attempting to initialize...') @@ -405,29 +426,42 @@ class UnifiedStorage { return this.initPromise } + private async executeWithAutoFallback(operation: () => Promise): Promise { + try { + return await operation() + } catch (error) { + if (this.backend === 'flask') { + console.warn('[Storage] Flask operation failed, switching to IndexedDB:', error) + await this.switchToIndexedDB() + return await operation() + } + throw error + } + } + async get(key: string): Promise { await this.detectAndInitialize() - return this.adapter!.get(key) + return this.executeWithAutoFallback(() => this.adapter!.get(key)) } async set(key: string, value: T): Promise { await this.detectAndInitialize() - await this.adapter!.set(key, value) + return this.executeWithAutoFallback(() => this.adapter!.set(key, value)) } async delete(key: string): Promise { await this.detectAndInitialize() - await this.adapter!.delete(key) + return this.executeWithAutoFallback(() => this.adapter!.delete(key)) } async keys(): Promise { await this.detectAndInitialize() - return this.adapter!.keys() + return this.executeWithAutoFallback(() => this.adapter!.keys()) } async clear(): Promise { await this.detectAndInitialize() - await this.adapter!.clear() + return this.executeWithAutoFallback(() => this.adapter!.clear()) } async getBackend(): Promise {