Files
low-code-react-app-b/docs/STORAGE_FALLBACK_IMPLEMENTATION.md
2026-01-17 20:41:48 +00:00

368 lines
9.7 KiB
Markdown

# Storage Fallback Implementation
## Summary
Enhanced the storage adapter system to **automatically fallback to IndexedDB** when Flask backend fetch operations fail. This ensures the application remains functional even when the backend is temporarily unavailable or experiencing issues.
## Problem
Previously, when the Flask backend was configured but became unavailable during runtime:
- All storage operations would throw errors
- The application would fail to save/load data
- User experience would be degraded
- No automatic recovery mechanism existed
## Solution
Implemented a **transparent fallback mechanism** that:
1. Attempts operations on the Flask backend first (when configured)
2. Automatically retries failed operations using IndexedDB
3. Logs a warning on the first fallback (prevents console spam)
4. Ensures data persistence even during backend outages
5. Works silently without user intervention
## Changes Made
### 1. **Enhanced AutoStorageAdapter** (`src/lib/storage-adapter.ts`)
#### Added Fallback Infrastructure
```typescript
class AutoStorageAdapter implements StorageAdapter {
private adapter: StorageAdapter | null = null
private fallbackAdapter: IndexedDBAdapter | null = null // NEW
private backendType: 'flask' | 'indexeddb' | null = null
private hasWarnedAboutFallback = false // NEW
// ...
}
```
#### Fallback Initialization
```typescript
if (this.backendType === 'flask' && FLASK_BACKEND_URL) {
this.adapter = new FlaskBackendAdapter(FLASK_BACKEND_URL)
this.fallbackAdapter = new IndexedDBAdapter() // NEW: Always create fallback
console.log(`[StorageAdapter] Initialized with Flask backend: ${FLASK_BACKEND_URL} (with IndexedDB fallback)`)
}
```
#### Smart Execution Wrapper
```typescript
private async executeWithFallback<T>(
operation: () => Promise<T>,
fallbackOperation?: () => Promise<T>
): Promise<T> {
try {
return await operation() // Try primary backend
} catch (error) {
// If Flask failed and we have fallback, use it
if (this.backendType === 'flask' && this.fallbackAdapter && fallbackOperation) {
if (!this.hasWarnedAboutFallback) {
console.warn('[StorageAdapter] Flask backend operation failed, falling back to IndexedDB:', error)
this.hasWarnedAboutFallback = true // Only warn once
}
try {
return await fallbackOperation() // Retry with IndexedDB
} catch (fallbackError) {
console.error('[StorageAdapter] Fallback to IndexedDB also failed:', fallbackError)
throw fallbackError
}
}
throw error
}
}
```
#### Updated Storage Methods
All storage methods now use `executeWithFallback`:
```typescript
async get<T>(key: string): Promise<T | undefined> {
await this.initialize()
return this.executeWithFallback(
() => this.adapter!.get<T>(key),
this.fallbackAdapter ? () => this.fallbackAdapter!.get<T>(key) : undefined
)
}
async set<T>(key: string, value: T): Promise<void> {
await this.initialize()
return this.executeWithFallback(
() => this.adapter!.set(key, value),
this.fallbackAdapter ? () => this.fallbackAdapter!.set(key, value) : undefined
)
}
// Similar changes for: delete(), keys(), clear()
```
### 2. **Updated Documentation** (`FLASK_BACKEND_AUTO_DETECTION.md`)
Added new "Runtime Fallback" flow to explain automatic retry behavior.
## How It Works
### Scenario 1: Flask Backend Available
```
User calls: storage.set('key', 'value')
AutoStorageAdapter.set()
executeWithFallback()
Try: FlaskBackendAdapter.set() → ✓ Success
Return
```
### Scenario 2: Flask Backend Fails (Network Error)
```
User calls: storage.get('key')
AutoStorageAdapter.get()
executeWithFallback()
Try: FlaskBackendAdapter.get() → ✗ Network Error
Catch error → Check if fallback available
Log warning (first time only)
Try: IndexedDBAdapter.get() → ✓ Success
Return data from IndexedDB
```
### Scenario 3: Flask Backend Timeout
```
User calls: storage.keys()
AutoStorageAdapter.keys()
executeWithFallback()
Try: FlaskBackendAdapter.keys() → ✗ Timeout after 3s
Catch timeout error
Try: IndexedDBAdapter.keys() → ✓ Success
Return keys from IndexedDB
```
### Scenario 4: IndexedDB Only (No Flask)
```
User calls: storage.set('key', 'value')
AutoStorageAdapter.set()
executeWithFallback()
Try: IndexedDBAdapter.set() → ✓ Success
Return (no fallback needed)
```
## Benefits
### 1. **Resilience**
- Application continues working during backend outages
- No data loss when network is unstable
- Graceful degradation of service
### 2. **User Experience**
- No visible errors to end users
- Seamless operation regardless of backend status
- Data always persists (either backend or browser)
### 3. **Development**
- Easier local development (backend optional)
- Reduced error handling complexity in application code
- Automatic recovery without code changes
### 4. **Production**
- Zero-downtime deployments possible
- Backend maintenance doesn't break frontend
- Network issues handled transparently
## Console Output Examples
### First Fallback Event
```
[StorageAdapter] Initialized with Flask backend: http://backend:5001 (with IndexedDB fallback)
[StorageAdapter] Flask backend operation failed, falling back to IndexedDB: TypeError: Failed to fetch
```
### Subsequent Fallback Events (No Spam)
```
(silent - already warned once)
```
### Normal Operation
```
[StorageAdapter] Initialized with Flask backend: http://backend:5001 (with IndexedDB fallback)
(operations succeed silently)
```
## Error Handling
### Fetch Failures That Trigger Fallback
- Network errors (`Failed to fetch`)
- Timeout errors (> 3 seconds)
- HTTP errors (500, 502, 503, 504)
- CORS errors
- DNS resolution failures
### Operations That Don't Fallback
- Already using IndexedDB as primary backend
- IndexedDB operation also fails (throws error up)
## Testing
### Test Fallback Behavior
1. **Start app with Flask backend configured:**
```bash
echo "VITE_USE_FLASK_BACKEND=true" > .env
echo "VITE_FLASK_BACKEND_URL=http://localhost:5001" >> .env
npm run dev
```
2. **Stop Flask backend:**
```bash
# In backend terminal: Ctrl+C
```
3. **Try storage operations:**
```typescript
// Should fallback to IndexedDB automatically
await storage.set('test', { value: 123 })
const result = await storage.get('test')
console.log(result) // { value: 123 } - from IndexedDB
```
4. **Check console:**
```
[StorageAdapter] Flask backend operation failed, falling back to IndexedDB: TypeError: Failed to fetch
```
5. **Verify data in IndexedDB:**
- Open Chrome DevTools → Application → IndexedDB
- Check `codeforge-db` → `storage` → `test` key
### Test Normal Operation
1. **Start both frontend and backend:**
```bash
# Terminal 1
cd backend && python app.py
# Terminal 2
npm run dev
```
2. **Perform storage operations:**
```typescript
await storage.set('test', { value: 123 })
```
3. **Verify no fallback warnings:**
```
(console should be clean, no fallback messages)
```
4. **Verify data in Flask backend:**
```bash
curl http://localhost:5001/api/storage/test
# {"value": {"value": 123}}
```
## Migration Considerations
### Data Consistency During Fallback
When fallback occurs:
- **Reading**: IndexedDB may have stale data if Flask backend was updated
- **Writing**: Data written to IndexedDB won't sync to Flask automatically
### Best Practices
1. **For Critical Data:**
- Implement sync mechanism when backend recovers
- Use optimistic UI updates with background sync
- Show user notification when in fallback mode
2. **For Non-Critical Data:**
- Current implementation is sufficient
- Data persists locally until backend available
- Manual export/import available if needed
3. **For Production:**
- Monitor backend availability
- Set up alerts for fallback events
- Implement periodic health checks
## Future Enhancements
Potential improvements:
1. **Auto-Sync When Backend Recovers**
```typescript
// Periodically check backend health
// If recovered, sync IndexedDB → Flask
async autoSyncOnRecovery() {
if (this.hasUsedFallback && await this.checkBackendAvailable()) {
await this.syncIndexedDBToFlask()
}
}
```
2. **Conflict Resolution**
```typescript
// Handle cases where data changed in both backends
async resolveConflicts(key: string) {
const flaskValue = await flaskAdapter.get(key)
const indexedDBValue = await indexedDBAdapter.get(key)
// Implement merge strategy
}
```
3. **User Notification**
```typescript
// Show toast when fallback occurs
if (!this.hasWarnedAboutFallback) {
toast.warning('Backend unavailable. Using local storage.')
}
```
4. **Fallback Metrics**
```typescript
// Track fallback frequency
interface FallbackStats {
count: number
lastFallback: Date
operations: string[]
}
```
## Related Files
- `src/lib/storage-adapter.ts` - Implementation
- `FLASK_BACKEND_AUTO_DETECTION.md` - Overall architecture
- `STORAGE_DEFAULT_INDEXEDDB.md` - Default behavior
- `STORAGE.md` - Complete storage documentation
## Success Criteria
✅ Flask backend failures automatically fallback to IndexedDB
✅ No user-visible errors during backend outages
✅ Data persists during fallback periods
✅ Warning logged on first fallback only
✅ Zero code changes required in consuming code
✅ Backward compatible with existing deployments
✅ Works in all scenarios (dev, prod, offline)
## Conclusion
The storage adapter now provides **resilient, transparent fallback** from Flask backend to IndexedDB. This ensures the application remains functional regardless of backend availability, providing a better user experience and reducing operational complexity.