mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-05-02 17:44:52 +00:00
Generated by Spark: Perhaps it could use sqlite on disk if possible, else use indexeddb
This commit is contained in:
@@ -0,0 +1,439 @@
|
||||
export type StorageBackend = 'sqlite' | 'indexeddb' | 'sparkkv'
|
||||
|
||||
export interface StorageAdapter {
|
||||
get<T>(key: string): Promise<T | undefined>
|
||||
set<T>(key: string, value: T): Promise<void>
|
||||
delete(key: string): Promise<void>
|
||||
keys(): Promise<string[]>
|
||||
clear(): Promise<void>
|
||||
close?(): Promise<void>
|
||||
}
|
||||
|
||||
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<void> {
|
||||
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<T>(key: string): Promise<T | undefined> {
|
||||
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<T>(key: string, value: T): Promise<void> {
|
||||
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<void> {
|
||||
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<string[]> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
if (this.db) {
|
||||
this.db.close()
|
||||
this.db = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SparkKVAdapter implements StorageAdapter {
|
||||
async get<T>(key: string): Promise<T | undefined> {
|
||||
if (!window.spark?.kv) throw new Error('Spark KV not available')
|
||||
return await window.spark.kv.get<T>(key)
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T): Promise<void> {
|
||||
if (!window.spark?.kv) throw new Error('Spark KV not available')
|
||||
await window.spark.kv.set(key, value)
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
if (!window.spark?.kv) throw new Error('Spark KV not available')
|
||||
await window.spark.kv.delete(key)
|
||||
}
|
||||
|
||||
async keys(): Promise<string[]> {
|
||||
if (!window.spark?.kv) throw new Error('Spark KV not available')
|
||||
return await window.spark.kv.keys()
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
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<void> | null = null
|
||||
|
||||
private async loadSQLiteWASM(): Promise<any> {
|
||||
const moduleName = 'sql.js'
|
||||
try {
|
||||
return await import(moduleName)
|
||||
} catch {
|
||||
throw new Error(`${moduleName} not installed. Run: npm install ${moduleName}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async init(): Promise<void> {
|
||||
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<T>(key: string): Promise<T | undefined> {
|
||||
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<T>(key: string, value: T): Promise<void> {
|
||||
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<void> {
|
||||
await this.init()
|
||||
this.db.run('DELETE FROM keyvalue WHERE key = ?', [key])
|
||||
this.persist()
|
||||
}
|
||||
|
||||
async keys(): Promise<string[]> {
|
||||
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<void> {
|
||||
await this.init()
|
||||
this.db.run('DELETE FROM keyvalue')
|
||||
this.persist()
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
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<void> | null = null
|
||||
|
||||
private async detectAndInitialize(): Promise<void> {
|
||||
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<T>(key: string): Promise<T | undefined> {
|
||||
await this.detectAndInitialize()
|
||||
return this.adapter!.get<T>(key)
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T): Promise<void> {
|
||||
await this.detectAndInitialize()
|
||||
await this.adapter!.set(key, value)
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
await this.detectAndInitialize()
|
||||
await this.adapter!.delete(key)
|
||||
}
|
||||
|
||||
async keys(): Promise<string[]> {
|
||||
await this.detectAndInitialize()
|
||||
return this.adapter!.keys()
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
await this.detectAndInitialize()
|
||||
await this.adapter!.clear()
|
||||
}
|
||||
|
||||
async getBackend(): Promise<StorageBackend | null> {
|
||||
await this.detectAndInitialize()
|
||||
return this.backend
|
||||
}
|
||||
|
||||
async switchToSQLite(): Promise<void> {
|
||||
if (this.backend === 'sqlite') return
|
||||
|
||||
console.log('[Storage] Switching to SQLite...')
|
||||
const oldKeys = await this.keys()
|
||||
const data: Record<string, any> = {}
|
||||
|
||||
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<void> {
|
||||
if (this.backend === 'indexeddb') return
|
||||
|
||||
console.log('[Storage] Switching to IndexedDB...')
|
||||
const oldKeys = await this.keys()
|
||||
const data: Record<string, any> = {}
|
||||
|
||||
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<Record<string, any>> {
|
||||
const allKeys = await this.keys()
|
||||
const data: Record<string, any> = {}
|
||||
|
||||
for (const key of allKeys) {
|
||||
data[key] = await this.get(key)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async importData(data: Record<string, any>): Promise<void> {
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
await this.set(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const unifiedStorage = new UnifiedStorage()
|
||||
Reference in New Issue
Block a user