305 lines
8.6 KiB
TypeScript
305 lines
8.6 KiB
TypeScript
/**
|
|
* Storage utility that uses IndexedDB for table data and localStorage for config/secrets
|
|
*
|
|
* Strategy:
|
|
* - IndexedDB: ALL table/datasource data (can store hundreds of MB)
|
|
* - localStorage: Only for secrets, variables, configurations (small data)
|
|
*/
|
|
|
|
const DB_NAME = 'analitica-nucleo-db'
|
|
const DB_VERSION = 1
|
|
const STORE_NAME = 'table-data'
|
|
|
|
let dbInstance: IDBDatabase | null = null
|
|
|
|
/**
|
|
* Initialize IndexedDB
|
|
*/
|
|
async function initDB(): Promise<IDBDatabase> {
|
|
if (dbInstance) return dbInstance
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open(DB_NAME, DB_VERSION)
|
|
|
|
request.onerror = () => {
|
|
console.error('[Storage] IndexedDB error:', request.error)
|
|
reject(request.error)
|
|
}
|
|
|
|
request.onsuccess = () => {
|
|
dbInstance = request.result
|
|
console.log('[Storage] IndexedDB initialized successfully')
|
|
resolve(dbInstance)
|
|
}
|
|
|
|
request.onupgradeneeded = (event) => {
|
|
const db = (event.target as IDBOpenDBRequest).result
|
|
|
|
// Create object store if it doesn't exist
|
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
db.createObjectStore(STORE_NAME, { keyPath: 'key' })
|
|
console.log('[Storage] Created object store:', STORE_NAME)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Deep clone to remove reactivity and make data serializable for IndexedDB
|
|
*/
|
|
function toPlainObject(obj: any): any {
|
|
// Use JSON parse/stringify to deep clone and remove all reactivity/proxies
|
|
try {
|
|
return JSON.parse(JSON.stringify(obj))
|
|
} catch (error) {
|
|
console.error('[Storage] Failed to convert to plain object:', error)
|
|
return obj
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save data to IndexedDB
|
|
*/
|
|
async function saveToIndexedDB(key: string, data: any): Promise<boolean> {
|
|
try {
|
|
const db = await initDB()
|
|
|
|
// Convert reactive objects to plain objects
|
|
const plainData = toPlainObject(data)
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = db.transaction([STORE_NAME], 'readwrite')
|
|
const store = transaction.objectStore(STORE_NAME)
|
|
|
|
const request = store.put({
|
|
key,
|
|
value: plainData,
|
|
timestamp: Date.now()
|
|
})
|
|
|
|
request.onsuccess = () => {
|
|
console.log(`[Storage] Saved to IndexedDB: ${key}`)
|
|
resolve(true)
|
|
}
|
|
|
|
request.onerror = () => {
|
|
console.error(`[Storage] Failed to save to IndexedDB: ${key}`, request.error)
|
|
reject(request.error)
|
|
}
|
|
})
|
|
} catch (error) {
|
|
console.error('[Storage] IndexedDB save error:', error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load data from IndexedDB
|
|
*/
|
|
async function loadFromIndexedDB(key: string): Promise<any | null> {
|
|
try {
|
|
const db = await initDB()
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = db.transaction([STORE_NAME], 'readonly')
|
|
const store = transaction.objectStore(STORE_NAME)
|
|
const request = store.get(key)
|
|
|
|
request.onsuccess = () => {
|
|
if (request.result) {
|
|
console.log(`[Storage] Loaded from IndexedDB: ${key}`)
|
|
resolve(request.result.value)
|
|
} else {
|
|
console.log(`[Storage] No data found in IndexedDB for: ${key}`)
|
|
resolve(null)
|
|
}
|
|
}
|
|
|
|
request.onerror = () => {
|
|
console.error(`[Storage] Failed to load from IndexedDB: ${key}`, request.error)
|
|
reject(request.error)
|
|
}
|
|
})
|
|
} catch (error) {
|
|
console.error('[Storage] IndexedDB load error:', error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove data from IndexedDB
|
|
*/
|
|
async function removeFromIndexedDB(key: string): Promise<boolean> {
|
|
try {
|
|
const db = await initDB()
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = db.transaction([STORE_NAME], 'readwrite')
|
|
const store = transaction.objectStore(STORE_NAME)
|
|
const request = store.delete(key)
|
|
|
|
request.onsuccess = () => {
|
|
console.log(`[Storage] Removed from IndexedDB: ${key}`)
|
|
resolve(true)
|
|
}
|
|
|
|
request.onerror = () => {
|
|
console.error(`[Storage] Failed to remove from IndexedDB: ${key}`, request.error)
|
|
reject(request.error)
|
|
}
|
|
})
|
|
} catch (error) {
|
|
console.error('[Storage] IndexedDB remove error:', error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Storage interface for table/datasource data (uses IndexedDB exclusively)
|
|
*/
|
|
export const tableDataStorage = {
|
|
/**
|
|
* Save table data to IndexedDB (for large datasets)
|
|
*/
|
|
async setItem(key: string, data: any): Promise<void> {
|
|
if (!process.client) {
|
|
console.log('[TableDataStorage] Not on client, skipping storage')
|
|
return
|
|
}
|
|
|
|
const dataString = JSON.stringify(data)
|
|
const dataSizeKB = new Blob([dataString]).size / 1024
|
|
console.log(`[TableDataStorage] Saving ${dataSizeKB.toFixed(2)} KB to IndexedDB for key: ${key}`)
|
|
|
|
try {
|
|
const success = await saveToIndexedDB(key, data)
|
|
if (success) {
|
|
console.log(`[TableDataStorage] ✓ Successfully saved ${dataSizeKB.toFixed(2)} KB to IndexedDB`)
|
|
return
|
|
} else {
|
|
throw new Error('Failed to save to IndexedDB')
|
|
}
|
|
} catch (error) {
|
|
console.error('[TableDataStorage] ✗ Failed to save to IndexedDB:', error)
|
|
throw new Error(`No se pudo guardar la tabla: ${error}`)
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load table data from IndexedDB
|
|
*/
|
|
async getItem(key: string): Promise<any | null> {
|
|
if (!process.client) {
|
|
console.log('[TableDataStorage] Not on client, skipping storage')
|
|
return null
|
|
}
|
|
|
|
console.log(`[TableDataStorage] Loading from IndexedDB: ${key}`)
|
|
|
|
try {
|
|
const data = await loadFromIndexedDB(key)
|
|
if (data !== null) {
|
|
const dataString = JSON.stringify(data)
|
|
const dataSizeKB = new Blob([dataString]).size / 1024
|
|
console.log(`[TableDataStorage] ✓ Loaded ${dataSizeKB.toFixed(2)} KB from IndexedDB`)
|
|
return data
|
|
} else {
|
|
console.log(`[TableDataStorage] No data found in IndexedDB for: ${key}`)
|
|
return null
|
|
}
|
|
} catch (error) {
|
|
console.error('[TableDataStorage] ✗ Failed to load from IndexedDB:', error)
|
|
return null
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Remove table data from IndexedDB
|
|
*/
|
|
async removeItem(key: string): Promise<void> {
|
|
if (!process.client) return
|
|
|
|
console.log(`[TableDataStorage] Removing from IndexedDB: ${key}`)
|
|
|
|
try {
|
|
await removeFromIndexedDB(key)
|
|
console.log(`[TableDataStorage] ✓ Removed from IndexedDB: ${key}`)
|
|
} catch (error) {
|
|
console.error('[TableDataStorage] ✗ Failed to remove from IndexedDB:', error)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Storage interface for config/secrets (uses localStorage exclusively)
|
|
*/
|
|
export const configStorage = {
|
|
/**
|
|
* Save config/secrets to localStorage (for small data only)
|
|
*/
|
|
setItem(key: string, data: any): void {
|
|
if (!process.client) {
|
|
console.log('[ConfigStorage] Not on client, skipping storage')
|
|
return
|
|
}
|
|
|
|
const dataString = JSON.stringify(data)
|
|
const dataSizeKB = new Blob([dataString]).size / 1024
|
|
console.log(`[ConfigStorage] Saving ${dataSizeKB.toFixed(2)} KB to localStorage for key: ${key}`)
|
|
|
|
try {
|
|
localStorage.setItem(key, dataString)
|
|
console.log(`[ConfigStorage] ✓ Saved to localStorage`)
|
|
} catch (error: any) {
|
|
if (error.name === 'QuotaExceededError') {
|
|
console.error('[ConfigStorage] ✗ localStorage quota exceeded')
|
|
throw new Error('No hay espacio en localStorage para guardar la configuración')
|
|
}
|
|
throw error
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Load config/secrets from localStorage
|
|
*/
|
|
getItem(key: string): any | null {
|
|
if (!process.client) {
|
|
console.log('[ConfigStorage] Not on client, skipping storage')
|
|
return null
|
|
}
|
|
|
|
console.log(`[ConfigStorage] Loading from localStorage: ${key}`)
|
|
|
|
try {
|
|
const item = localStorage.getItem(key)
|
|
if (item) {
|
|
const data = JSON.parse(item)
|
|
const dataSizeKB = new Blob([item]).size / 1024
|
|
console.log(`[ConfigStorage] ✓ Loaded ${dataSizeKB.toFixed(2)} KB from localStorage`)
|
|
return data
|
|
} else {
|
|
console.log(`[ConfigStorage] No data found in localStorage for: ${key}`)
|
|
return null
|
|
}
|
|
} catch (error) {
|
|
console.error('[ConfigStorage] ✗ Failed to load from localStorage:', error)
|
|
return null
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Remove config/secrets from localStorage
|
|
*/
|
|
removeItem(key: string): void {
|
|
if (!process.client) return
|
|
|
|
console.log(`[ConfigStorage] Removing from localStorage: ${key}`)
|
|
|
|
try {
|
|
localStorage.removeItem(key)
|
|
console.log(`[ConfigStorage] ✓ Removed from localStorage: ${key}`)
|
|
} catch (error) {
|
|
console.error('[ConfigStorage] ✗ Failed to remove from localStorage:', error)
|
|
}
|
|
}
|
|
} |