From 02eb47e83fb1cdd2f98ccd90131599aaac800da8 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sat, 17 Jan 2026 18:19:45 +0000 Subject: [PATCH] Generated by Spark: Perhaps it could use sqlite on disk if possible, else use indexeddb --- STORAGE.md | 320 +++++++++++++++++ src/components/StorageSettings.tsx | 5 +- src/components/StorageSettingsPanel.tsx | 257 ++++++++++++++ src/hooks/use-unified-storage.ts | 153 +++++++++ src/lib/unified-storage.ts | 439 ++++++++++++++++++++++++ 5 files changed, 1173 insertions(+), 1 deletion(-) create mode 100644 STORAGE.md create mode 100644 src/components/StorageSettingsPanel.tsx create mode 100644 src/hooks/use-unified-storage.ts create mode 100644 src/lib/unified-storage.ts diff --git a/STORAGE.md b/STORAGE.md new file mode 100644 index 0000000..735632d --- /dev/null +++ b/STORAGE.md @@ -0,0 +1,320 @@ +# Unified Storage System + +CodeForge now features a unified storage system that automatically selects the best available storage backend for your data persistence needs. + +## Storage Backends + +The system supports three storage backends in order of preference: + +### 1. **SQLite (Preferred)** +- **Type**: On-disk database via WASM +- **Persistence**: Data stored in browser localStorage as serialized SQLite database +- **Pros**: + - SQL query support + - Better performance for complex queries + - More robust data integrity + - Works offline +- **Cons**: + - Requires sql.js library (optional dependency) + - Slightly larger bundle size + - localStorage size limits (~5-10MB) +- **Installation**: `npm install sql.js` + +### 2. **IndexedDB (Default)** +- **Type**: Browser-native key-value store +- **Persistence**: Data stored in browser IndexedDB +- **Pros**: + - No additional dependencies + - Large storage capacity (usually >50MB, can be GBs) + - Fast for simple key-value operations + - Works offline + - Native browser support +- **Cons**: + - No SQL query support + - More complex API + - Asynchronous only + +### 3. **Spark KV (Fallback)** +- **Type**: Cloud key-value store +- **Persistence**: Data stored in Spark runtime +- **Pros**: + - No size limits + - Synced across devices + - Persistent beyond browser +- **Cons**: + - Requires Spark runtime + - Online only + - Slower than local storage + +## Usage + +### Basic Usage + +```typescript +import { unifiedStorage } from '@/lib/unified-storage' + +// Get data +const value = await unifiedStorage.get('my-key') + +// Set data +await unifiedStorage.set('my-key', myData) + +// Delete data +await unifiedStorage.delete('my-key') + +// Get all keys +const keys = await unifiedStorage.keys() + +// Clear all data +await unifiedStorage.clear() + +// Check current backend +const backend = await unifiedStorage.getBackend() +console.log(`Using: ${backend}`) // 'sqlite', 'indexeddb', or 'sparkkv' +``` + +### React Hook + +```typescript +import { useUnifiedStorage } from '@/hooks/use-unified-storage' + +function MyComponent() { + const [todos, setTodos, deleteTodos] = useUnifiedStorage('todos', []) + + const addTodo = async (todo: Todo) => { + // ALWAYS use functional updates to avoid stale data + await setTodos((current) => [...current, todo]) + } + + const removeTodo = async (id: string) => { + await setTodos((current) => current.filter(t => t.id !== id)) + } + + return ( +
+ + +
+ ) +} +``` + +### Storage Backend Management + +```typescript +import { useStorageBackend } from '@/hooks/use-unified-storage' + +function StorageManager() { + const { + backend, + isLoading, + switchToSQLite, + switchToIndexedDB, + exportData, + importData, + } = useStorageBackend() + + return ( +
+

Current backend: {backend}

+ + + +
+ ) +} +``` + +## Migration Between Backends + +The system supports seamless migration between storage backends: + +```typescript +// Migrate from IndexedDB to SQLite (preserves all data) +await unifiedStorage.switchToSQLite() + +// Migrate from SQLite to IndexedDB (preserves all data) +await unifiedStorage.switchToIndexedDB() +``` + +When switching backends: +1. All existing data is exported from the current backend +2. The new backend is initialized +3. All data is imported into the new backend +4. The preference is saved for future sessions + +## Data Export/Import + +Export and import data for backup or migration purposes: + +```typescript +// Export all data as JSON +const data = await unifiedStorage.exportData() +const json = JSON.stringify(data, null, 2) + +// Save to file +const blob = new Blob([json], { type: 'application/json' }) +const url = URL.createObjectURL(blob) +const a = document.createElement('a') +a.href = url +a.download = 'codeforge-backup.json' +a.click() + +// Import data from JSON +const imported = JSON.parse(jsonString) +await unifiedStorage.importData(imported) +``` + +## Backend Detection + +The system automatically detects and selects the best available backend on initialization: + +1. **SQLite** is attempted first if `localStorage.getItem('codeforge-prefer-sqlite') === 'true'` +2. **IndexedDB** is attempted next if available in the browser +3. **Spark KV** is used as a last resort fallback + +You can check which backend is in use: + +```typescript +const backend = await unifiedStorage.getBackend() +// Returns: 'sqlite' | 'indexeddb' | 'sparkkv' | null +``` + +## Performance Considerations + +### SQLite +- Best for: Complex queries, relational data, large datasets +- Read: Fast (in-memory queries) +- Write: Moderate (requires serialization to localStorage) +- Capacity: Limited by localStorage (~5-10MB) + +### IndexedDB +- Best for: Simple key-value storage, large data volumes +- Read: Very fast (optimized for key lookups) +- Write: Very fast (optimized browser API) +- Capacity: Large (typically 50MB+, can scale to GBs) + +### Spark KV +- Best for: Cross-device sync, cloud persistence +- Read: Moderate (network latency) +- Write: Moderate (network latency) +- Capacity: Unlimited + +## Troubleshooting + +### SQLite Not Available + +If SQLite fails to initialize: +1. Check console for errors +2. Ensure sql.js is installed: `npm install sql.js` +3. System will automatically fallback to IndexedDB + +### IndexedDB Quota Exceeded + +If IndexedDB storage is full: +1. Clear old data: `await unifiedStorage.clear()` +2. Export important data first +3. Consider switching to Spark KV for unlimited storage + +### Data Not Persisting + +1. Check which backend is active: `await unifiedStorage.getBackend()` +2. Verify browser supports storage (check if in private mode) +3. Check browser console for errors +4. Try exporting/importing data to refresh storage + +## Best Practices + +1. **Use Functional Updates**: Always use functional form of setState to avoid stale data: + ```typescript + // ❌ WRONG - can lose data + setTodos([...todos, newTodo]) + + // ✅ CORRECT - always safe + setTodos((current) => [...current, newTodo]) + ``` + +2. **Handle Errors**: Wrap storage operations in try-catch: + ```typescript + try { + await unifiedStorage.set('key', value) + } catch (error) { + console.error('Storage failed:', error) + toast.error('Failed to save data') + } + ``` + +3. **Export Regularly**: Create backups of important data: + ```typescript + const backup = await unifiedStorage.exportData() + // Save backup somewhere safe + ``` + +4. **Use Appropriate Backend**: Choose based on your needs: + - Local-only, small data → IndexedDB + - Local-only, needs SQL → SQLite (install sql.js) + - Cloud sync needed → Spark KV + +## UI Component + +The app includes a `StorageSettingsPanel` component that provides a user-friendly interface for: +- Viewing current storage backend +- Switching between backends +- Exporting/importing data +- Viewing storage statistics + +Add it to your settings page: + +```typescript +import { StorageSettingsPanel } from '@/components/StorageSettingsPanel' + +function SettingsPage() { + return ( +
+

Settings

+ +
+ ) +} +``` + +## Architecture + +``` +┌─────────────────────────────────────────┐ +│ Unified Storage API │ +│ (unifiedStorage.get/set/delete/keys) │ +└──────────────┬──────────────────────────┘ + │ + ├─ Automatic Backend Detection + │ + ┌───────┴───────┬─────────────┬────────┐ + │ │ │ │ + ▼ ▼ ▼ ▼ +┌─────────────┐ ┌────────────┐ ┌─────────┐ ┌────┐ +│ SQLite │ │ IndexedDB │ │Spark KV │ │ ? │ +│ (optional) │ │ (default) │ │(fallback│ │Next│ +└─────────────┘ └────────────┘ └─────────┘ └────┘ + │ │ │ + └───────┬───────┴─────────────┘ + │ + ▼ + Browser Storage +``` + +## Future Enhancements + +- [ ] Add compression for large data objects +- [ ] Implement automatic backup scheduling +- [ ] Add support for native file system API +- [ ] Support for WebSQL (legacy browsers) +- [ ] Encrypted storage option +- [ ] Storage analytics and usage metrics +- [ ] Automatic data migration on version changes diff --git a/src/components/StorageSettings.tsx b/src/components/StorageSettings.tsx index f7bc3a2..601ff3e 100644 --- a/src/components/StorageSettings.tsx +++ b/src/components/StorageSettings.tsx @@ -7,6 +7,7 @@ import { Database, HardDrive, CloudArrowUp, CloudArrowDown, Trash, Info } from ' import { storage } from '@/lib/storage' import { db } from '@/lib/db' import { toast } from 'sonner' +import { StorageSettingsPanel } from './StorageSettingsPanel' export function StorageSettings() { const [isMigrating, setIsMigrating] = useState(false) @@ -108,11 +109,13 @@ export function StorageSettings() {

+ + - Storage Information + Legacy Storage Information This application uses IndexedDB as the primary local database, with Spark KV as a diff --git a/src/components/StorageSettingsPanel.tsx b/src/components/StorageSettingsPanel.tsx new file mode 100644 index 0000000..0836c96 --- /dev/null +++ b/src/components/StorageSettingsPanel.tsx @@ -0,0 +1,257 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { useStorageBackend } from '@/hooks/use-unified-storage' +import { Database, HardDrive, Cloud, Download, Upload, CircleNotch } from '@phosphor-icons/react' +import { toast } from 'sonner' +import { useState } from 'react' + +export function StorageSettingsPanel() { + const { + backend, + isLoading, + switchToSQLite, + switchToIndexedDB, + exportData, + importData, + } = useStorageBackend() + + const [isSwitching, setIsSwitching] = useState(false) + const [isExporting, setIsExporting] = useState(false) + const [isImporting, setIsImporting] = useState(false) + + const handleSwitchToSQLite = async () => { + if (backend === 'sqlite') { + toast.info('Already using SQLite') + return + } + + setIsSwitching(true) + try { + await switchToSQLite() + toast.success('Switched to SQLite storage') + } catch (error) { + toast.error(`Failed to switch to SQLite: ${error instanceof Error ? error.message : 'Unknown error'}`) + } finally { + setIsSwitching(false) + } + } + + const handleSwitchToIndexedDB = async () => { + if (backend === 'indexeddb') { + toast.info('Already using IndexedDB') + return + } + + setIsSwitching(true) + try { + await switchToIndexedDB() + toast.success('Switched to IndexedDB storage') + } catch (error) { + toast.error(`Failed to switch to IndexedDB: ${error instanceof Error ? error.message : 'Unknown error'}`) + } finally { + setIsSwitching(false) + } + } + + const handleExport = async () => { + setIsExporting(true) + try { + const data = await exportData() + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `codeforge-data-${new Date().toISOString().split('T')[0]}.json` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + toast.success('Data exported successfully') + } catch (error) { + toast.error(`Failed to export data: ${error instanceof Error ? error.message : 'Unknown error'}`) + } finally { + setIsExporting(false) + } + } + + const handleImport = async () => { + const input = document.createElement('input') + input.type = 'file' + input.accept = 'application/json' + + input.onchange = async (e) => { + const file = (e.target as HTMLInputElement).files?.[0] + if (!file) return + + setIsImporting(true) + try { + const text = await file.text() + const data = JSON.parse(text) + await importData(data) + toast.success('Data imported successfully') + } catch (error) { + toast.error(`Failed to import data: ${error instanceof Error ? error.message : 'Unknown error'}`) + } finally { + setIsImporting(false) + } + } + + input.click() + } + + const getBackendIcon = () => { + switch (backend) { + case 'sqlite': + return + case 'indexeddb': + return + case 'sparkkv': + return + default: + return + } + } + + const getBackendLabel = () => { + switch (backend) { + case 'sqlite': + return 'SQLite (On-disk)' + case 'indexeddb': + return 'IndexedDB (Browser)' + case 'sparkkv': + return 'Spark KV (Cloud)' + default: + return 'Unknown' + } + } + + const getBackendDescription = () => { + switch (backend) { + case 'sqlite': + return 'Data stored in SQLite database persisted to localStorage' + case 'indexeddb': + return 'Data stored in browser IndexedDB (recommended for most users)' + case 'sparkkv': + return 'Data stored in Spark cloud key-value store' + default: + return 'No storage backend detected' + } + } + + if (isLoading && !backend) { + return ( + + + + + Storage Settings + + Detecting storage backend... + + +
+ +
+
+
+ ) + } + + return ( + + + + + Storage Settings + + + Manage your local data storage preferences + + + +
+
+
+ Current Backend: + {getBackendIcon()} + {getBackendLabel()} +
+ {backend?.toUpperCase() || 'UNKNOWN'} +
+

{getBackendDescription()}

+
+ +
+

Switch Storage Backend

+
+ + +
+

+ Switching storage backends will migrate all existing data +

+
+ +
+

Data Management

+
+ + +
+

+ Export your data as a JSON file or import from a previous backup +

+
+
+
+ ) +} diff --git a/src/hooks/use-unified-storage.ts b/src/hooks/use-unified-storage.ts new file mode 100644 index 0000000..cbce30e --- /dev/null +++ b/src/hooks/use-unified-storage.ts @@ -0,0 +1,153 @@ +import { useState, useEffect, useCallback } from 'react' +import { unifiedStorage } from '@/lib/unified-storage' + +export function useUnifiedStorage( + key: string, + defaultValue: T +): [T, (value: T | ((prev: T) => T)) => Promise, () => Promise] { + const [value, setValue] = useState(defaultValue) + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + let mounted = true + + const loadValue = async () => { + try { + const stored = await unifiedStorage.get(key) + if (mounted) { + setValue(stored !== undefined ? stored : defaultValue) + } + } catch (error) { + console.error(`Failed to load ${key}:`, error) + if (mounted) { + setValue(defaultValue) + } + } finally { + if (mounted) { + setIsLoading(false) + } + } + } + + loadValue() + + return () => { + mounted = false + } + }, [key, defaultValue]) + + const updateValue = useCallback( + async (newValue: T | ((prev: T) => T)) => { + try { + const valueToSet = typeof newValue === 'function' + ? (newValue as (prev: T) => T)(value) + : newValue + + setValue(valueToSet) + await unifiedStorage.set(key, valueToSet) + } catch (error) { + console.error(`Failed to save ${key}:`, error) + throw error + } + }, + [key, value] + ) + + const deleteValue = useCallback(async () => { + try { + setValue(defaultValue) + await unifiedStorage.delete(key) + } catch (error) { + console.error(`Failed to delete ${key}:`, error) + throw error + } + }, [key, defaultValue]) + + return [value, updateValue, deleteValue] +} + +export function useStorageBackend() { + const [backend, setBackend] = useState(null) + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + let mounted = true + + const detectBackend = async () => { + try { + const currentBackend = await unifiedStorage.getBackend() + if (mounted) { + setBackend(currentBackend) + } + } catch (error) { + console.error('Failed to detect storage backend:', error) + } finally { + if (mounted) { + setIsLoading(false) + } + } + } + + detectBackend() + + return () => { + mounted = false + } + }, []) + + const switchToSQLite = useCallback(async () => { + setIsLoading(true) + try { + await unifiedStorage.switchToSQLite() + setBackend('sqlite') + } catch (error) { + console.error('Failed to switch to SQLite:', error) + throw error + } finally { + setIsLoading(false) + } + }, []) + + const switchToIndexedDB = useCallback(async () => { + setIsLoading(true) + try { + await unifiedStorage.switchToIndexedDB() + setBackend('indexeddb') + } catch (error) { + console.error('Failed to switch to IndexedDB:', error) + throw error + } finally { + setIsLoading(false) + } + }, []) + + const exportData = useCallback(async () => { + try { + return await unifiedStorage.exportData() + } catch (error) { + console.error('Failed to export data:', error) + throw error + } + }, []) + + const importData = useCallback(async (data: Record) => { + setIsLoading(true) + try { + await unifiedStorage.importData(data) + } catch (error) { + console.error('Failed to import data:', error) + throw error + } finally { + setIsLoading(false) + } + }, []) + + return { + backend, + isLoading, + switchToSQLite, + switchToIndexedDB, + exportData, + importData, + } +} diff --git a/src/lib/unified-storage.ts b/src/lib/unified-storage.ts new file mode 100644 index 0000000..8c73788 --- /dev/null +++ b/src/lib/unified-storage.ts @@ -0,0 +1,439 @@ +export type StorageBackend = 'sqlite' | 'indexeddb' | 'sparkkv' + +export interface StorageAdapter { + get(key: string): Promise + set(key: string, value: T): Promise + delete(key: string): Promise + keys(): Promise + clear(): Promise + close?(): Promise +} + +class IndexedDBAdapter implements StorageAdapter { + private db: IDBDatabase | null = null + private readonly dbName = 'CodeForgeDB' + private readonly storeName = 'keyvalue' + private readonly version = 2 + + private async init(): Promise { + if (this.db) return + + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.dbName, this.version) + + request.onerror = () => reject(request.error) + request.onsuccess = () => { + this.db = request.result + resolve() + } + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result + + if (!db.objectStoreNames.contains(this.storeName)) { + db.createObjectStore(this.storeName, { keyPath: 'key' }) + } + } + }) + } + + async get(key: string): Promise { + await this.init() + if (!this.db) throw new Error('Database not initialized') + + return new Promise((resolve, reject) => { + const transaction = this.db!.transaction(this.storeName, 'readonly') + const store = transaction.objectStore(this.storeName) + const request = store.get(key) + + request.onerror = () => reject(request.error) + request.onsuccess = () => { + const result = request.result + resolve(result ? result.value : undefined) + } + }) + } + + async set(key: string, value: T): Promise { + await this.init() + if (!this.db) throw new Error('Database not initialized') + + return new Promise((resolve, reject) => { + const transaction = this.db!.transaction(this.storeName, 'readwrite') + const store = transaction.objectStore(this.storeName) + const request = store.put({ key, value }) + + request.onerror = () => reject(request.error) + request.onsuccess = () => resolve() + }) + } + + async delete(key: string): Promise { + await this.init() + if (!this.db) throw new Error('Database not initialized') + + return new Promise((resolve, reject) => { + const transaction = this.db!.transaction(this.storeName, 'readwrite') + const store = transaction.objectStore(this.storeName) + const request = store.delete(key) + + request.onerror = () => reject(request.error) + request.onsuccess = () => resolve() + }) + } + + async keys(): Promise { + await this.init() + if (!this.db) throw new Error('Database not initialized') + + return new Promise((resolve, reject) => { + const transaction = this.db!.transaction(this.storeName, 'readonly') + const store = transaction.objectStore(this.storeName) + const request = store.getAllKeys() + + request.onerror = () => reject(request.error) + request.onsuccess = () => resolve(request.result as string[]) + }) + } + + async clear(): Promise { + await this.init() + if (!this.db) throw new Error('Database not initialized') + + return new Promise((resolve, reject) => { + const transaction = this.db!.transaction(this.storeName, 'readwrite') + const store = transaction.objectStore(this.storeName) + const request = store.clear() + + request.onerror = () => reject(request.error) + request.onsuccess = () => resolve() + }) + } + + async close(): Promise { + if (this.db) { + this.db.close() + this.db = null + } + } +} + +class SparkKVAdapter implements StorageAdapter { + async get(key: string): Promise { + if (!window.spark?.kv) throw new Error('Spark KV not available') + return await window.spark.kv.get(key) + } + + async set(key: string, value: T): Promise { + if (!window.spark?.kv) throw new Error('Spark KV not available') + await window.spark.kv.set(key, value) + } + + async delete(key: string): Promise { + if (!window.spark?.kv) throw new Error('Spark KV not available') + await window.spark.kv.delete(key) + } + + async keys(): Promise { + if (!window.spark?.kv) throw new Error('Spark KV not available') + return await window.spark.kv.keys() + } + + async clear(): Promise { + if (!window.spark?.kv) throw new Error('Spark KV not available') + const allKeys = await window.spark.kv.keys() + await Promise.all(allKeys.map(key => window.spark.kv.delete(key))) + } +} + +class SQLiteAdapter implements StorageAdapter { + private db: any = null + private SQL: any = null + private initPromise: Promise | null = null + + private async loadSQLiteWASM(): Promise { + const moduleName = 'sql.js' + try { + return await import(moduleName) + } catch { + throw new Error(`${moduleName} not installed. Run: npm install ${moduleName}`) + } + } + + private async init(): Promise { + if (this.db) return + if (this.initPromise) return this.initPromise + + this.initPromise = (async () => { + try { + const sqlJsModule = await this.loadSQLiteWASM() + const initSqlJs = sqlJsModule.default + + this.SQL = await initSqlJs({ + locateFile: (file: string) => `https://sql.js.org/dist/${file}` + }) + + const data = localStorage.getItem('codeforge-sqlite-db') + if (data) { + const buffer = new Uint8Array(JSON.parse(data)) + this.db = new this.SQL.Database(buffer) + } else { + this.db = new this.SQL.Database() + } + + this.db.run(` + CREATE TABLE IF NOT EXISTS keyvalue ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + `) + } catch (error) { + console.error('SQLite initialization failed:', error) + throw error + } + })() + + return this.initPromise + } + + private persist(): void { + if (!this.db) return + try { + const data = this.db.export() + const buffer = Array.from(data) + localStorage.setItem('codeforge-sqlite-db', JSON.stringify(buffer)) + } catch (error) { + console.error('Failed to persist SQLite database:', error) + } + } + + async get(key: string): Promise { + await this.init() + const stmt = this.db.prepare('SELECT value FROM keyvalue WHERE key = ?') + stmt.bind([key]) + + if (stmt.step()) { + const row = stmt.getAsObject() + stmt.free() + return JSON.parse(row.value as string) as T + } + + stmt.free() + return undefined + } + + async set(key: string, value: T): Promise { + await this.init() + this.db.run( + 'INSERT OR REPLACE INTO keyvalue (key, value) VALUES (?, ?)', + [key, JSON.stringify(value)] + ) + this.persist() + } + + async delete(key: string): Promise { + await this.init() + this.db.run('DELETE FROM keyvalue WHERE key = ?', [key]) + this.persist() + } + + async keys(): Promise { + await this.init() + const stmt = this.db.prepare('SELECT key FROM keyvalue') + const keys: string[] = [] + + while (stmt.step()) { + const row = stmt.getAsObject() + keys.push(row.key as string) + } + + stmt.free() + return keys + } + + async clear(): Promise { + await this.init() + this.db.run('DELETE FROM keyvalue') + this.persist() + } + + async close(): Promise { + if (this.db) { + this.persist() + this.db.close() + this.db = null + this.SQL = null + this.initPromise = null + } + } +} + +class UnifiedStorage { + private adapter: StorageAdapter | null = null + private backend: StorageBackend | null = null + private initPromise: Promise | null = null + + private async detectAndInitialize(): Promise { + if (this.adapter) return + if (this.initPromise) return this.initPromise + + this.initPromise = (async () => { + const preferSQLite = localStorage.getItem('codeforge-prefer-sqlite') === 'true' + + if (preferSQLite) { + try { + console.log('[Storage] Attempting to initialize SQLite...') + const sqliteAdapter = new SQLiteAdapter() + await sqliteAdapter.get('_health_check') + this.adapter = sqliteAdapter + this.backend = 'sqlite' + console.log('[Storage] ✓ Using SQLite') + return + } catch (error) { + console.warn('[Storage] SQLite not available:', error) + } + } + + if (typeof indexedDB !== 'undefined') { + try { + console.log('[Storage] Attempting to initialize IndexedDB...') + const idbAdapter = new IndexedDBAdapter() + await idbAdapter.get('_health_check') + this.adapter = idbAdapter + this.backend = 'indexeddb' + console.log('[Storage] ✓ Using IndexedDB') + return + } catch (error) { + console.warn('[Storage] IndexedDB not available:', error) + } + } + + if (window.spark?.kv) { + try { + console.log('[Storage] Attempting to initialize Spark KV...') + const sparkAdapter = new SparkKVAdapter() + await sparkAdapter.get('_health_check') + this.adapter = sparkAdapter + this.backend = 'sparkkv' + console.log('[Storage] ✓ Using Spark KV') + return + } catch (error) { + console.warn('[Storage] Spark KV not available:', error) + } + } + + throw new Error('No storage backend available') + })() + + return this.initPromise + } + + async get(key: string): Promise { + await this.detectAndInitialize() + return this.adapter!.get(key) + } + + async set(key: string, value: T): Promise { + await this.detectAndInitialize() + await this.adapter!.set(key, value) + } + + async delete(key: string): Promise { + await this.detectAndInitialize() + await this.adapter!.delete(key) + } + + async keys(): Promise { + await this.detectAndInitialize() + return this.adapter!.keys() + } + + async clear(): Promise { + await this.detectAndInitialize() + await this.adapter!.clear() + } + + async getBackend(): Promise { + await this.detectAndInitialize() + return this.backend + } + + async switchToSQLite(): Promise { + if (this.backend === 'sqlite') return + + console.log('[Storage] Switching to SQLite...') + const oldKeys = await this.keys() + const data: Record = {} + + for (const key of oldKeys) { + data[key] = await this.get(key) + } + + if (this.adapter?.close) { + await this.adapter.close() + } + + this.adapter = null + this.backend = null + this.initPromise = null + + localStorage.setItem('codeforge-prefer-sqlite', 'true') + + await this.detectAndInitialize() + + for (const [key, value] of Object.entries(data)) { + await this.set(key, value) + } + + console.log('[Storage] ✓ Migrated to SQLite') + } + + async switchToIndexedDB(): Promise { + if (this.backend === 'indexeddb') return + + console.log('[Storage] Switching to IndexedDB...') + const oldKeys = await this.keys() + const data: Record = {} + + for (const key of oldKeys) { + data[key] = await this.get(key) + } + + if (this.adapter?.close) { + await this.adapter.close() + } + + this.adapter = null + this.backend = null + this.initPromise = null + + localStorage.removeItem('codeforge-prefer-sqlite') + + await this.detectAndInitialize() + + for (const [key, value] of Object.entries(data)) { + await this.set(key, value) + } + + console.log('[Storage] ✓ Migrated to IndexedDB') + } + + async exportData(): Promise> { + const allKeys = await this.keys() + const data: Record = {} + + for (const key of allKeys) { + data[key] = await this.get(key) + } + + return data + } + + async importData(data: Record): Promise { + for (const [key, value] of Object.entries(data)) { + await this.set(key, value) + } + } +} + +export const unifiedStorage = new UnifiedStorage()