Merge pull request #28 from johndoe6345789/codex/extract-adapter-classes-to-separate-files

refactor(unified-storage): extract adapters into separate files
This commit is contained in:
2026-01-18 00:20:24 +00:00
committed by GitHub
7 changed files with 357 additions and 344 deletions

View File

@@ -0,0 +1,77 @@
import type { StorageAdapter } from './types'
export 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 controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), this.TIMEOUT_MS)
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
}
}
async get<T>(key: string): Promise<T | undefined> {
try {
const result = await this.request<{ value: T }>(`/api/storage/${encodeURIComponent(key)}`)
return result.value
} catch (error: any) {
if (error.message?.includes('404') || error.message?.includes('not found')) {
return undefined
}
throw error
}
}
async set<T>(key: string, value: T): Promise<void> {
await this.request(`/api/storage/${encodeURIComponent(key)}`, {
method: 'PUT',
body: JSON.stringify({ value }),
})
}
async delete(key: string): Promise<void> {
await this.request(`/api/storage/${encodeURIComponent(key)}`, {
method: 'DELETE',
})
}
async keys(): Promise<string[]> {
const result = await this.request<{ keys: string[] }>('/api/storage/keys')
return result.keys
}
async clear(): Promise<void> {
await this.request('/api/storage/clear', {
method: 'POST',
})
}
}

View File

@@ -0,0 +1,5 @@
export { FlaskBackendAdapter } from './flask-backend-adapter'
export { IndexedDBAdapter } from './indexeddb-adapter'
export { SparkKVAdapter } from './spark-kv-adapter'
export { SQLiteAdapter } from './sqlite-adapter'
export type { StorageAdapter, StorageBackend } from './types'

View File

@@ -0,0 +1,110 @@
import type { StorageAdapter } from './types'
export 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
}
}
}

View File

@@ -0,0 +1,29 @@
import type { StorageAdapter } from './types'
export 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)))
}
}

View File

@@ -0,0 +1,123 @@
import type { StorageAdapter } from './types'
export 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(/* @vite-ignore */ 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
}
}
}

View File

@@ -0,0 +1,10 @@
export type StorageBackend = 'flask' | 'indexeddb' | 'sqlite' | '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>
}

View File

@@ -1,350 +1,9 @@
/// <reference path="../global.d.ts" />
export type StorageBackend = 'flask' | 'indexeddb' | 'sqlite' | 'sparkkv'
import type { StorageAdapter, StorageBackend } from './unified-storage-adapters'
import { FlaskBackendAdapter, IndexedDBAdapter, SparkKVAdapter, SQLiteAdapter } from './unified-storage-adapters'
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 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 controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), this.TIMEOUT_MS)
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
}
}
async get<T>(key: string): Promise<T | undefined> {
try {
const result = await this.request<{ value: T }>(`/api/storage/${encodeURIComponent(key)}`)
return result.value
} catch (error: any) {
if (error.message?.includes('404') || error.message?.includes('not found')) {
return undefined
}
throw error
}
}
async set<T>(key: string, value: T): Promise<void> {
await this.request(`/api/storage/${encodeURIComponent(key)}`, {
method: 'PUT',
body: JSON.stringify({ value }),
})
}
async delete(key: string): Promise<void> {
await this.request(`/api/storage/${encodeURIComponent(key)}`, {
method: 'DELETE',
})
}
async keys(): Promise<string[]> {
const result = await this.request<{ keys: string[] }>('/api/storage/keys')
return result.keys
}
async clear(): Promise<void> {
await this.request('/api/storage/clear', {
method: 'POST',
})
}
}
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(/* @vite-ignore */ 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
}
}
}
export type { StorageAdapter, StorageBackend } from './unified-storage-adapters'
class UnifiedStorage {
private adapter: StorageAdapter | null = null