IndexedDB en JavaScript: almacenamiento estructurado | Nicolás Garzón
IndexedDB
Texto
Copiar Database
└─ Object Store
├─ Records
└─ IndexesContenedor versionado identificado por un nombre.
Colección de registros. Se parece a una tabla en su función organizativa, pero IndexedDB no es una base SQL relacional.
Identificador del registro. Puede venir de:
Una clave explícita.
Una propiedad mediante keyPath.
Un generador autoIncrement.
Estructura adicional para buscar por otra propiedad.
Contexto donde se realizan operaciones sobre uno o varios stores.
JavaScript
Copiar const request = indexedDB. open ( "app-db" , 1 ) ; La API nativa utiliza objetos request y eventos, no promesas directamente.
JavaScript
Copiar request. onsuccess = ( ) => {
const database = request. result;
} ;
request. onerror = ( ) => {
console. error ( request. error) ;
} ; JavaScript
Copiar request. onupgradeneeded = ( ) => {
const database = request. result;
const products = database. createObjectStore (
"products" ,
{
keyPath : "id" ,
} ,
) ;
products. createIndex (
"by-status" ,
"status" ,
) ;
} ; onupgradeneeded se ejecuta cuando se crea la base o se abre con una versión mayor.
Los cambios de esquema deben realizarse dentro de esa transaction de upgrade.
JavaScript
Copiar database. createObjectStore ( "products" ) ; Fuera de una versionchange transaction produce un error. El esquema está ligado al proceso de versión.
Si otra pestaña mantiene una conexión antigua abierta, el upgrade puede quedar bloqueado.
JavaScript
Copiar request. onblocked = ( ) => {
showMessage (
"Close other tabs to finish updating the database" ,
) ;
} ; JavaScript
Copiar database. onversionchange = ( ) => {
database. close ( ) ;
} ; Esto permite que otra pestaña complete la migración.
JavaScript
Copiar const transaction = database. transaction (
"products" ,
"readonly" ,
) ; JavaScript
Copiar const transaction = database. transaction (
[ "products" , "inventory" ] ,
"readwrite" ,
) ; Incluye desde el inicio todos los stores que la operación necesita.
JavaScript
Copiar const store = transaction. objectStore ( "products" ) ; JavaScript
Copiar store. add ( product) ; add falla si la clave ya existe.
JavaScript
Copiar store. put ( product) ; put inserta o reemplaza según la clave.
Elige según el contrato; put puede ocultar una colisión que debería ser error.
JavaScript
Copiar store. get ( productId) ;
store. getAll ( ) ;
store. count ( ) ; Un request exitoso puede devolver undefined cuando no existe el registro. Error de request y ausencia son resultados distintos.
JavaScript
Copiar store. delete ( productId) ;
store. clear ( ) ; clear elimina todos los registros del store; no borra su esquema ni índices.
JavaScript
Copiar const index = store. index ( "by-status" ) ;
const activeProducts = index. getAll ( "active" ) ;
unique.
multiEntry para valores de array.
Cada índice aumenta trabajo de escritura y almacenamiento. Crea índices para consultas reales.
JavaScript
Copiar const range = IDBKeyRange. bound (
10 ,
20 ,
false ,
true ,
) ;
index. getAll ( range) ; Permiten consultas por intervalos de claves.
JavaScript
Copiar const request = index. openCursor ( ) ;
request. onsuccess = ( ) => {
const cursor = request. result;
if ( ! cursor) return ;
processProduct ( cursor. value) ;
cursor. continue ( ) ;
} ; Un cursor procesa gradualmente y permite actualizar o eliminar el registro actual según el modo de la transaction.
IndexedDB mantiene activa una transaction mientras existen requests pendientes y el flujo de eventos continúa según sus reglas.
JavaScript
Copiar const transaction = database. transaction (
"products" ,
"readwrite" ,
) ;
await fetchSomething ( ) ;
transaction. objectStore ( "products" ) . put ( product) ; Durante el await, la transaction puede haberse auto-committed y quedar inactiva.
Obtén datos externos antes o realiza la operación de IndexedDB sin esperas ajenas dentro de la transaction.
Una transaction completa cuando todas sus operaciones terminan correctamente.
JavaScript
Copiar transaction. oncomplete = ( ) => { } ;
transaction. onerror = ( ) => { } ;
transaction. onabort = ( ) => { } ; JavaScript
Copiar transaction. abort ( ) ; Las escrituras realizadas dentro de la transaction no quedan parcialmente confirmadas.
Un error no manejado de un request puede abortar la transaction. En casos específicos puede cancelarse la propagación del evento de error, pero hacerlo sin entender la consistencia puede producir datos incompletos.
IndexedDB guarda valores mediante el algoritmo structured clone.
Puede conservar estructuras como:
Objects y arrays.
Date.
Map y Set.
ArrayBuffer y TypedArrays.
Blobs y Files en contextos compatibles.
Functions.
DOM nodes.
WeakMap o WeakSet.
Los prototipos personalizados no deben asumirse preservados como instancias de clase con métodos.
JavaScript
Copiar const object = { nested : { } } ;
store. put ( object) ; Lo almacenado es una representación clonada. Leerlo después produce objetos distintos, no las mismas referencias de memoria.
La capacidad depende del navegador, dispositivo, origen, espacio disponible y políticas.
JavaScript
Copiar const estimate = await navigator. storage. estimate ( ) ; Para solicitar almacenamiento menos susceptible a eviction:
JavaScript
Copiar const persisted = await navigator. storage. persist ( ) ; El navegador decide si concede persistencia. No sustituye sincronización con servidor ni backups.
IndexedDB está separada por origen, pero cualquier script que se ejecute en ese origen puede acceder según su contexto.
No la trates como bóveda secreta. XSS, extensiones, dispositivo comprometido o acceso local pueden exponer datos.
Guarda solo lo necesario y define retención y limpieza.
IndexedDB es local a un perfil y dispositivo. No resuelve:
Conflictos entre dispositivos.
Fuente de verdad remota.
Merge de cambios.
Autorización del servidor.
Una app offline necesita un protocolo de sincronización explícito.
La API nativa es basada en eventos y tiene detalles complejos. Librerías como idb o Dexie pueden ofrecer promesas y transacciones más cómodas.
La abstracción no elimina la necesidad de entender:
Versiones.
Vida de transactions.
Índices.
Cuota.
Consistencia.
JavaScript
Copiar function openDatabase ( ) {
return new Promise ( ( resolve, reject ) => {
const request = indexedDB. open ( "app-db" , 1 ) ;
request. onupgradeneeded = ( ) => {
const database = request. result;
if ( ! database. objectStoreNames. contains ( "products" ) ) {
database. createObjectStore ( "products" , {
keyPath : "id" ,
} ) ;
}
} ;
request. onsuccess = ( ) => {
const database = request. result;
database. onversionchange = ( ) => {
database. close ( ) ;
} ;
resolve ( database) ;
} ;
request. onerror = ( ) => {
reject ( request. error) ;
} ;
} ) ;
}
Describir IndexedDB como MongoDB dentro del navegador.
Modificar stores fuera de onupgradeneeded.
Ignorar pestañas que bloquean upgrades.
Usar put cuando una colisión debería fallar.
Crear índices sin consultas que los necesiten.
Hacer awaits externos dentro de una transaction activa.
Confundir ausencia con error de request.
Creer que conserva instancias y prototipos personalizados.
Asumir una cuota fija o persistencia garantizada.
Guardar datos sensibles sin estrategia de riesgo y retención.
Pensar que almacenamiento local equivale a sincronización.
IndexedDB es asíncrona, estructurada y transaccional.
El esquema cambia mediante versiones y upgrade transactions.
Stores guardan registros; índices soportan consultas.
add y put tienen contratos diferentes.
Las transactions pueden cerrarse durante awaits ajenos.
Structured clone conserva más tipos que JSON, pero no cualquier objeto.
La cuota y persistencia dependen del navegador.
Los datos locales necesitan estrategia de sincronización y limpieza.
¿Por qué una transaction puede fallar después de hacer await fetch(...) antes de escribir?
Respuesta Porque IndexedDB puede considerar que ya no existen requests pendientes y completar automáticamente la transaction mientras la función espera una operación externa.
Same-origin, CORS y seguridad de red explica por qué el navegador limita interacciones entre orígenes.