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 (
+
+ addTodo({ id: '1', text: 'New Todo' })}>
+ Add Todo
+
+ Clear All
+
+ )
+}
+```
+
+### 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}
+
Switch to SQLite
+
Switch to IndexedDB
+
{
+ const data = await exportData()
+ console.log('Exported:', data)
+ }}>
+ Export Data
+
+
+ )
+}
+```
+
+## 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
+
+
+ {isSwitching ? (
+
+ ) : (
+
+ )}
+ SQLite
+
+
+ {isSwitching ? (
+
+ ) : (
+
+ )}
+ IndexedDB
+
+
+
+ Switching storage backends will migrate all existing data
+
+
+
+
+
Data Management
+
+
+ {isExporting ? (
+
+ ) : (
+
+ )}
+ Export Data
+
+
+ {isImporting ? (
+
+ ) : (
+
+ )}
+ Import Data
+
+
+
+ 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()