mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Generated by Spark: A library in packages folder - Seems its still trying to use fetch. If a fetch fails, switch back to IndexedDB.
This commit is contained in:
@@ -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<Response> {
|
||||
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<boolean> {
|
||||
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<void> | null = null
|
||||
private hasWarnedAboutFallback = false
|
||||
private failureCount = 0
|
||||
private readonly MAX_FAILURES_BEFORE_SWITCH = 3
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
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<T>(
|
||||
operation: () => Promise<T>,
|
||||
fallbackOperation?: () => Promise<T>
|
||||
): Promise<T> {
|
||||
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) {
|
||||
|
||||
@@ -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<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
||||
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<T>(key: string): Promise<T | undefined> {
|
||||
@@ -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<T>(operation: () => Promise<T>): Promise<T> {
|
||||
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<T>(key: string): Promise<T | undefined> {
|
||||
await this.detectAndInitialize()
|
||||
return this.adapter!.get<T>(key)
|
||||
return this.executeWithAutoFallback(() => this.adapter!.get<T>(key))
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T): Promise<void> {
|
||||
await this.detectAndInitialize()
|
||||
await this.adapter!.set(key, value)
|
||||
return this.executeWithAutoFallback(() => this.adapter!.set(key, value))
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
await this.detectAndInitialize()
|
||||
await this.adapter!.delete(key)
|
||||
return this.executeWithAutoFallback(() => this.adapter!.delete(key))
|
||||
}
|
||||
|
||||
async keys(): Promise<string[]> {
|
||||
await this.detectAndInitialize()
|
||||
return this.adapter!.keys()
|
||||
return this.executeWithAutoFallback(() => this.adapter!.keys())
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
await this.detectAndInitialize()
|
||||
await this.adapter!.clear()
|
||||
return this.executeWithAutoFallback(() => this.adapter!.clear())
|
||||
}
|
||||
|
||||
async getBackend(): Promise<StorageBackend | null> {
|
||||
|
||||
Reference in New Issue
Block a user