Files
analiticaNucleo/nuxt4-app/app/utils/storage.ts
2025-09-30 02:02:56 -06:00

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)
}
}
}