All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m6s
- Agregar clonación en saveSession() y updateSession() - Mantener mutaciones directas en el estado reactivo - Resolver error DataCloneError al guardar objetos reactivos en IndexedDB
363 lines
9.9 KiB
TypeScript
363 lines
9.9 KiB
TypeScript
/**
|
|
* Composable para manejo de sesiones de catación en IndexedDB
|
|
* Maneja solo una sesión activa a la vez
|
|
*/
|
|
|
|
import type { SesionCatacion } from '~/types/catacion'
|
|
|
|
const DB_NAME = 'RioCataDB'
|
|
const DB_VERSION = 1
|
|
const STORE_NAME = 'sesiones'
|
|
const ACTIVE_SESSION_KEY = 'sesion-activa'
|
|
|
|
/**
|
|
* Inicializa y retorna la base de datos IndexedDB
|
|
*/
|
|
function openDatabase(): Promise<IDBDatabase> {
|
|
return new Promise((resolve, reject) => {
|
|
// Verificar si estamos en el cliente
|
|
if (typeof window === 'undefined' || !window.indexedDB) {
|
|
reject(new Error('IndexedDB no está disponible en este entorno'))
|
|
return
|
|
}
|
|
|
|
const request = window.indexedDB.open(DB_NAME, DB_VERSION)
|
|
|
|
request.onerror = () => {
|
|
reject(new Error('Error al abrir la base de datos'))
|
|
}
|
|
|
|
request.onsuccess = () => {
|
|
resolve(request.result)
|
|
}
|
|
|
|
request.onupgradeneeded = (event) => {
|
|
const db = (event.target as IDBOpenDBRequest).result
|
|
|
|
// Crear object store si no existe
|
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
const objectStore = db.createObjectStore(STORE_NAME, { keyPath: 'sessionId' })
|
|
// Crear índice por fecha para consultas futuras
|
|
objectStore.createIndex('fecha', 'fecha', { unique: false })
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Guarda o actualiza la sesión activa en IndexedDB
|
|
*/
|
|
async function saveSession(sesion: SesionCatacion): Promise<void> {
|
|
try {
|
|
// Clonar la sesión para eliminar los proxies reactivos de Vue
|
|
// antes de guardar en IndexedDB
|
|
const sesionClonada = JSON.parse(JSON.stringify(sesion)) as SesionCatacion
|
|
|
|
const db = await openDatabase()
|
|
const transaction = db.transaction([STORE_NAME], 'readwrite')
|
|
const objectStore = transaction.objectStore(STORE_NAME)
|
|
|
|
// Eliminar todas las sesiones anteriores
|
|
await new Promise<void>((resolve, reject) => {
|
|
const clearRequest = objectStore.clear()
|
|
clearRequest.onsuccess = () => resolve()
|
|
clearRequest.onerror = () => reject(new Error('Error al limpiar sesiones anteriores'))
|
|
})
|
|
|
|
// Guardar la nueva sesión
|
|
await new Promise<void>((resolve, reject) => {
|
|
const addRequest = objectStore.add(sesionClonada)
|
|
addRequest.onsuccess = () => resolve()
|
|
addRequest.onerror = () => reject(new Error('Error al guardar la sesión'))
|
|
})
|
|
|
|
db.close()
|
|
} catch (error) {
|
|
console.error('Error en saveSession:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migra una sesión del formato antiguo al nuevo
|
|
*/
|
|
function migrateSesion(sesion: any): SesionCatacion {
|
|
// Migrar cada muestra
|
|
const muestrasMigradas = sesion.muestras.map((muestra: any) => {
|
|
const muestraMigrada = { ...muestra }
|
|
|
|
// Migrar fraganciaAromaNotas
|
|
if (muestra.fraganciaAromaNotas) {
|
|
const notas = muestra.fraganciaAromaNotas
|
|
// Si tiene el formato antiguo (categoria como string)
|
|
if ('categoria' in notas && typeof notas.categoria === 'string') {
|
|
muestraMigrada.fraganciaAromaNotas = {
|
|
categorias: notas.categoria ? [notas.categoria] : [],
|
|
subcategorias: notas.subcategoria ? [notas.subcategoria] : [],
|
|
notaEspecifica: notas.notaEspecifica,
|
|
}
|
|
}
|
|
// Si ya tiene el formato nuevo pero es null
|
|
else if (notas.categorias === null || notas.categorias === undefined) {
|
|
muestraMigrada.fraganciaAromaNotas = {
|
|
categorias: [],
|
|
subcategorias: [],
|
|
notaEspecifica: notas.notaEspecifica || null,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Migrar saborNotas
|
|
if (muestra.saborNotas) {
|
|
const notas = muestra.saborNotas
|
|
// Si tiene el formato antiguo (categoria como string)
|
|
if ('categoria' in notas && typeof notas.categoria === 'string') {
|
|
muestraMigrada.saborNotas = {
|
|
categorias: notas.categoria ? [notas.categoria] : [],
|
|
subcategorias: notas.subcategoria ? [notas.subcategoria] : [],
|
|
notaEspecifica: notas.notaEspecifica,
|
|
}
|
|
}
|
|
// Si ya tiene el formato nuevo pero es null
|
|
else if (notas.categorias === null || notas.categorias === undefined) {
|
|
muestraMigrada.saborNotas = {
|
|
categorias: [],
|
|
subcategorias: [],
|
|
notaEspecifica: notas.notaEspecifica || null,
|
|
}
|
|
}
|
|
}
|
|
|
|
return muestraMigrada
|
|
})
|
|
|
|
return {
|
|
...sesion,
|
|
muestras: muestrasMigradas,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Carga la sesión activa desde IndexedDB
|
|
*/
|
|
async function loadSession(): Promise<SesionCatacion | null> {
|
|
try {
|
|
const db = await openDatabase()
|
|
const transaction = db.transaction([STORE_NAME], 'readonly')
|
|
const objectStore = transaction.objectStore(STORE_NAME)
|
|
|
|
const sesion = await new Promise<SesionCatacion | null>((resolve, reject) => {
|
|
// Obtener todas las claves
|
|
const getAllRequest = objectStore.getAll()
|
|
|
|
getAllRequest.onsuccess = () => {
|
|
const sesiones = getAllRequest.result as SesionCatacion[]
|
|
// Retornar la primera sesión (debería haber solo una)
|
|
let sesion = sesiones.length > 0 ? sesiones[0] : null
|
|
|
|
// Migrar sesión si es necesario
|
|
if (sesion) {
|
|
sesion = migrateSesion(sesion)
|
|
}
|
|
|
|
resolve(sesion || null)
|
|
}
|
|
|
|
getAllRequest.onerror = () => {
|
|
reject(new Error('Error al cargar la sesión'))
|
|
}
|
|
})
|
|
|
|
db.close()
|
|
return sesion
|
|
} catch (error) {
|
|
console.error('Error en loadSession:', error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verifica si existe una sesión activa
|
|
*/
|
|
async function hasActiveSession(): Promise<boolean> {
|
|
try {
|
|
const sesion = await loadSession()
|
|
return sesion !== null
|
|
} catch (error) {
|
|
console.error('Error en hasActiveSession:', error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Elimina la sesión activa
|
|
*/
|
|
async function deleteSession(): Promise<void> {
|
|
try {
|
|
const db = await openDatabase()
|
|
const transaction = db.transaction([STORE_NAME], 'readwrite')
|
|
const objectStore = transaction.objectStore(STORE_NAME)
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
const clearRequest = objectStore.clear()
|
|
clearRequest.onsuccess = () => resolve()
|
|
clearRequest.onerror = () => reject(new Error('Error al eliminar la sesión'))
|
|
})
|
|
|
|
db.close()
|
|
} catch (error) {
|
|
console.error('Error en deleteSession:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Actualiza una sesión existente (timestamp de modificación)
|
|
*/
|
|
async function updateSession(sesion: SesionCatacion): Promise<void> {
|
|
try {
|
|
// Actualizar timestamp de modificación
|
|
sesion.modificadoEn = Date.now()
|
|
|
|
// Clonar la sesión para eliminar los proxies reactivos de Vue
|
|
// antes de guardar en IndexedDB
|
|
const sesionClonada = JSON.parse(JSON.stringify(sesion)) as SesionCatacion
|
|
|
|
const db = await openDatabase()
|
|
const transaction = db.transaction([STORE_NAME], 'readwrite')
|
|
const objectStore = transaction.objectStore(STORE_NAME)
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
const putRequest = objectStore.put(sesionClonada)
|
|
putRequest.onsuccess = () => resolve()
|
|
putRequest.onerror = () => reject(new Error('Error al actualizar la sesión'))
|
|
})
|
|
|
|
db.close()
|
|
} catch (error) {
|
|
console.error('Error en updateSession:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Composable principal para manejo de IndexedDB
|
|
*/
|
|
export const useIndexedDB = () => {
|
|
// Estado reactivo de la sesión
|
|
const sesionActiva = useState<SesionCatacion | null>('sesion-activa', () => null)
|
|
const cargando = useState<boolean>('sesion-cargando', () => false)
|
|
const error = useState<Error | null>('sesion-error', () => null)
|
|
|
|
/**
|
|
* Inicializa el composable cargando la sesión activa
|
|
*/
|
|
const inicializar = async () => {
|
|
if (import.meta.server) {
|
|
// No hacer nada en el servidor
|
|
return
|
|
}
|
|
|
|
try {
|
|
cargando.value = true
|
|
error.value = null
|
|
const sesion = await loadSession()
|
|
sesionActiva.value = sesion
|
|
} catch (err) {
|
|
error.value = err as Error
|
|
console.error('Error al inicializar IndexedDB:', err)
|
|
} finally {
|
|
cargando.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Guarda la sesión activa
|
|
*/
|
|
const guardar = async (sesion: SesionCatacion) => {
|
|
if (import.meta.server) {
|
|
console.warn('No se puede guardar en IndexedDB desde el servidor')
|
|
return
|
|
}
|
|
|
|
try {
|
|
cargando.value = true
|
|
error.value = null
|
|
await saveSession(sesion)
|
|
sesionActiva.value = sesion
|
|
} catch (err) {
|
|
error.value = err as Error
|
|
console.error('Error al guardar sesión:', err)
|
|
throw err
|
|
} finally {
|
|
cargando.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Actualiza la sesión activa
|
|
*/
|
|
const actualizar = async (sesion: SesionCatacion) => {
|
|
if (import.meta.server) {
|
|
console.warn('No se puede actualizar en IndexedDB desde el servidor')
|
|
return
|
|
}
|
|
|
|
try {
|
|
cargando.value = true
|
|
error.value = null
|
|
await updateSession(sesion)
|
|
// NO reemplazar el objeto completo para mantener las referencias de Vue
|
|
// sesionActiva.value = sesion
|
|
} catch (err) {
|
|
error.value = err as Error
|
|
console.error('Error al actualizar sesión:', err)
|
|
throw err
|
|
} finally {
|
|
cargando.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Elimina la sesión activa
|
|
*/
|
|
const eliminar = async () => {
|
|
if (import.meta.server) {
|
|
console.warn('No se puede eliminar de IndexedDB desde el servidor')
|
|
return
|
|
}
|
|
|
|
try {
|
|
cargando.value = true
|
|
error.value = null
|
|
await deleteSession()
|
|
sesionActiva.value = null
|
|
} catch (err) {
|
|
error.value = err as Error
|
|
console.error('Error al eliminar sesión:', err)
|
|
throw err
|
|
} finally {
|
|
cargando.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verifica si hay una sesión activa
|
|
*/
|
|
const tieneSecion = computed(() => sesionActiva.value !== null)
|
|
|
|
return {
|
|
// Estado
|
|
sesionActiva, // No readonly para permitir mutaciones directas
|
|
cargando: readonly(cargando),
|
|
error: readonly(error),
|
|
tieneSecion,
|
|
|
|
// Métodos
|
|
inicializar,
|
|
guardar,
|
|
actualizar,
|
|
eliminar,
|
|
}
|
|
}
|