Ciclo de vida de IndexedDB: versiones, cuota y limpieza
Abre y versiona IndexedDB con seguridad, actualiza sin bloquear otras pestañas, planifica la cuota y el desalojo, y elimina los datos de la aplicación al cerrar sesión.
Quieres la documentación estructurada de Identidad?
Este artículo forma parte de la biblioteca editorial. Para pasos de configuración, material de referencia y actualizaciones continuas, entra en la sección de docs.
Abre una base de datos con una versión explícita
Una base de datos IndexedDB tiene un ciclo de vida con cuatro etapas que el código de la aplicación debe gestionar a propósito: abrirla con una versión, actualizar el esquema, vivir dentro de la cuota que administra el navegador y eliminarse cuando los datos ya no deban existir. Cada etapa tiene un evento o una API que informa de lo que ocurrió, y cada una tiene un desenlace de fallo que el usuario puede ver. Una aplicación fiable abre la base de datos con una versión explícita, cambia el esquema solo dentro del paso de actualización, cierra su conexión cuando otra pestaña pide una versión más reciente, registra qué ocurre cuando se agota el espacio y elimina las bases de datos de la cuenta anterior al cerrar sesión, con un resultado acotado cuando la eliminación queda bloqueada.
Todo lo que sigue trata de los datos que guarda tu propia aplicación. Leer o inspeccionar los datos almacenados por otro sitio es una tarea distinta, y los nombres de bases de datos y almacenes de los ejemplos son inventados, no tomados de un sitio real. Para ver dónde encaja IndexedDB junto a las cookies y Web Storage, consulta Cookies, localStorage e IndexedDB: dónde debe vivir el estado.
El punto de entrada es indexedDB.open(name, version). Devuelve una solicitud de apertura, no una base de datos. La conexión llega en el evento success de la solicitud, y cualquier creación o cambio del esquema ocurre en el evento upgradeneeded, que se dispara primero cuando la versión solicitada es mayor que la almacenada. Si la base de datos aún no existe, se crea con la versión solicitada y upgradeneeded se ejecuta una vez para construir los almacenes de objetos iniciales. La API de bases de datos indexadas del W3C define esta secuencia, y la guía de MDN para usar IndexedDB la recorre con ejemplos.
Pasa la versión de forma explícita y guárdala como una constante con nombre en el código que es dueño del esquema. Cuando se omite el argumento de versión, el navegador abre la base de datos existente en su versión actual, o crea una nueva en la primera versión, de modo que el código no puede pedir una actualización a propósito. Un entero que el equipo aumenta con cada cambio de esquema hace que el historial sea revisable: cada número corresponde a un conjunto conocido de almacenes de objetos e índices, y una solicitud de cambios que modifica el esquema también modifica la constante.
Una versión menor que la almacenada también es un desenlace definido. La solicitud de apertura falla con un VersionError. Ocurre cuando un usuario mantiene abierta una pestaña antigua mientras otra más reciente ya actualizó la base de datos, o cuando se ejecuta una copia anterior en caché del código de la aplicación después de una versión más nueva. Trata el error como un estado de la aplicación con un mensaje y una acción de recarga, y no reintentes en bucle con un número de versión adivinado.
Gestiona los resultados de la solicitud en un solo lugar. El evento success entrega la conexión, error informa de un fallo como un VersionError o un problema de almacenamiento del que el navegador no puede recuperarse, y blocked informa de que otras conexiones aún mantienen abierta la base de datos mientras una actualización espera. Envuelve la llamada de apertura en una función pequeña que devuelva una promesa, asocia el controlador de versionchange a la conexión antes de devolverla y haz que el resto de la aplicación pida la base de datos a esa función en lugar de abrir la suya. Un único responsable de abrir y cerrar mantiene fácil de razonar el ciclo de vida de la conexión.
Nombra las bases de datos y los almacenes de objetos según lo que contienen. Usa un nombre de base de datos estable elegido por la aplicación y, cuando un mismo origen atiende varias cuentas, incluye en el nombre una clave local opaca de la cuenta en lugar de un correo electrónico u otros datos personales. El nombre es visible en las herramientas para desarrolladores y suele aparecer en los informes de soporte, y es además el identificador que un flujo de cierre de sesión necesita más adelante para encontrar y eliminar los datos de una cuenta.
Actualiza el esquema sin bloquear otras pestañas
Los cambios de esquema solo se permiten dentro del controlador de upgradeneeded. Durante ese evento la conexión mantiene una transacción especial de cambio de versión, y solo dentro de ella puede el código llamar a createObjectStore, deleteObjectStore, createIndex o deleteIndex. Fuera de ella, esas llamadas lanzan un error. La especificación del W3C da al evento los valores oldVersion y newVersion, que permiten que un único controlador lleve una base de datos hacia adelante desde cualquier versión anterior.
Escribe el controlador como una secuencia de pasos según la versión anterior: si la versión anterior es menor que el primer objetivo, crea los almacenes iniciales; si es menor que el segundo, añade el nuevo índice; y así sucesivamente. Un usuario puede llegar desde cualquier versión anterior, así que cada paso debe ejecutarse correctamente en orden y por sí solo. Limita el controlador al trabajo de IndexedDB. La transacción de actualización termina cuando no tiene solicitudes pendientes, de modo que esperar una llamada de red o una promesa ajena dentro del controlador puede cerrar la transacción antes de que se ejecute el siguiente paso. Descarga los datos remotos después de que la solicitud de apertura tenga éxito, no durante la actualización.
Mover los registros existentes a una forma nueva es la parte arriesgada. Ejecuta la conversión dentro de la misma transacción de actualización recorriendo el almacén antiguo con un cursor, de modo que el paso se confirme por completo o no se confirme. Si el controlador lanza una excepción, o llama a abort() en la transacción, la versión no cambia y la solicitud de apertura falla con un AbortError, lo que deja intacto el esquema anterior. Mantén cada migración pequeña y pruébala con una base de datos creada en cada versión anterior que la aplicación aún admita, no solo con la más reciente.
La segunda pestaña es donde las actualizaciones suelen fallar. IndexedDB permite que una base de datos cambie de versión solo cuando no hay ninguna conexión anterior abierta. Cuando una pestaña pide una versión mayor, el navegador dispara versionchange en las conexiones que otras pestañas aún mantienen. La guía de MDN para usar IndexedDB indica que el controlador debe cerrar la conexión para que la otra página pueda actualizar. Si no lo hace, la nueva solicitud de apertura dispara blocked y queda pendiente, la actualización no se ejecuta y el usuario ve que la pestaña nueva se queda esperando en un estado de carga.
Un buen controlador hace dos cosas. Llama a db.close() de inmediato y después informa al usuario de lo ocurrido, por ejemplo mostrando que la aplicación se actualizó en otra pestaña y ofreciendo recargar. Cerrar libera la base de datos para la pestaña que está actualizando, y el mensaje explica por qué esta pestaña dejó de funcionar. No sigas usando una conexión después de cerrarla, porque las transacciones nuevas sobre ella lanzan un error. El mismo evento se dispara cuando una pestaña llama a deleteDatabase, así que un solo controlador cubre tanto las actualizaciones como las eliminaciones.
La pestaña que pidió la actualización también debe gestionar blocked. Muestra un aviso breve de que otra ventana sigue abierta con una versión anterior, mantén pendiente la solicitud de apertura y deja que la actualización termine cuando la otra conexión se cierre. Si la aplicación también ejecuta un service worker o un worker compartido que abre la base de datos, inclúyelo en la revisión. Un worker es otra conexión que puede mantener una actualización en espera, y necesita el mismo tratamiento de versionchange.
Planifica la cuota y el desalojo de datos
Los navegadores reparten una cantidad limitada de espacio en disco entre los orígenes, y los datos de IndexedDB viven dentro de ese presupuesto. El Storage Standard describe el almacenamiento de un origen como de mejor esfuerzo por defecto: el navegador puede eliminarlo bajo presión de almacenamiento sin preguntar. La página de MDN sobre cuotas y criterios de desalojo explica que los límites y el orden de desalojo varían según el navegador. Las cifras de capacidad difieren entre navegadores, dispositivos y versiones, así que la aplicación no debe depender de un número que no puede verificar.
Dos desenlaces necesitan una respuesta definida de la aplicación. El primero es una escritura que no cabe. Cuando una transacción no puede confirmarse porque se agotó el espacio, el fallo se notifica como un QuotaExceededError y la transacción se aborta. Escucha abort y error en la transacción, no solo en cada solicitud individual, y decide de antemano qué ve el usuario. Entre las respuestas razonables están descartar los registros en caché de menor valor, pausar la sincronización o pedir al usuario que libere espacio, según el tipo de datos.
El segundo desenlace son datos que simplemente han desaparecido. Un origen de mejor esfuerzo puede ser desalojado, y los navegadores que desalojan suelen eliminar los datos de un origen como una unidad, no unos pocos registros cada vez. La siguiente visita encuentra una base de datos vacía y la aplicación la abre de nuevo con la primera versión. Diseña para ese caso. Conserva un registro marcador o un almacén de metadatos para que el código de arranque distinga una instalación nueva de una desalojada, y reconstruye desde el servidor cuando los datos tengan una copia allí.
La llamada navigator.storage.estimate() devuelve valores aproximados de uso y cuota que ayudan a decidir cuándo recortar una caché, y navigator.storage.persist() pide al navegador que trate los datos del origen como persistentes. Ambas son estimaciones y solicitudes según el Storage Standard. El navegador puede rechazar la persistencia, puede preguntar al usuario o puede decidir sin mostrar ningún aviso. Registra en la revisión el resultado de la solicitud en lugar de suponer que se concedió, y conserva la misma vía de recuperación para el caso en que no se concedió.
Registra las decisiones de cada almacén de objetos en una tabla pequeña que viva junto al código del esquema. Para cada almacén, anota quién es su responsable, su regla de retención y qué hace la aplicación cuando se supera la cuota o los datos han sido desalojados. Un almacén de borradores que el usuario no ha sincronizado necesita una advertencia y una vía de exportación. Un almacén de respuestas del servidor en caché solo necesita reconstruirse. Un almacén de ajustes puede necesitar un valor por defecto. Las reglas de retención pertenecen a la misma tabla: una caché de elementos recientes puede podarse al arrancar recorriendo con un cursor un índice sobre un campo de marca de tiempo y eliminando los registros más antiguos que la edad indicada, en lotes pequeños para que cada transacción sea corta.
Elimina los datos de la aplicación al cerrar sesión y cambiar de cuenta
Cerrar sesión es una decisión sobre datos, no solo sobre la sesión. Cuando una persona cierra sesión en un ordenador compartido, o una cuenta sustituye a otra en el mismo perfil del navegador, los datos de IndexedDB de la cuenta anterior siguen en el disco bajo el mismo origen hasta que la aplicación los elimina. Un flujo de limpieza necesita una lista clara de lo que debe eliminarse, una manera de encontrarlo y un desenlace definido cuando la eliminación no puede terminar.
Mantén un registro de los nombres de las bases de datos que crea la aplicación, como una lista corta en el código o un registro de metadatos, en lugar de depender del descubrimiento. Cuando está disponible, indexedDB.databases() puede listar las bases de datos del origen y resulta útil para un paso de verificación, pero la compatibilidad de los navegadores ha variado con el tiempo, así que consulta las notas de compatibilidad antes de usarla como única fuente. Con un registro, cerrar sesión se convierte en un bucle: cierra las conexiones propias de esta pestaña y luego llama a indexedDB.deleteDatabase(name) para cada nombre que pertenezca a la cuenta que se va.
El método devuelve una solicitud, igual que open. Su página de referencia indica que la eliminación dispara versionchange en las conexiones abiertas y, si alguna sigue abierta, dispara blocked en la solicitud, y que la eliminación espera hasta que se cierren. Por eso el controlador de versionchange de la sección de actualización también importa aquí, y por eso un flujo de cierre de sesión que no cierra antes su propia conexión se bloqueará a sí mismo. Dale al flujo un desenlace acotado: espera success y, si llega blocked y no se resuelve dentro de una espera breve que elige la aplicación, registra que la purga está pendiente, informa al usuario y reintenta en el siguiente arranque antes de leer cualquier dato de la cuenta anterior. Evita un indicador de carga sin fin en la pantalla de cierre de sesión.
Un servidor también puede pedir al navegador que borre datos con la cabecera de respuesta Clear-Site-Data. La directiva "storage" abarca IndexedDB junto con otro almacenamiento del origen, como localStorage y los registros de service workers, lo que significa que borra más que IndexedDB y encaja mejor en un cierre de sesión completo que en la eliminación de una cuenta entre varias. La referencia de la cabecera señala que solo se respeta en respuestas seguras y que la compatibilidad difiere entre navegadores. Las conexiones abiertas pueden retrasar o limitar lo que se borra, así que conserva la eliminación del lado de la aplicación descrita arriba como la vía fiable y trata la cabecera como un paso adicional.
Ninguna de las dos herramientas demuestra que no quede ninguna copia de los datos del usuario. Los registros del servidor, las copias de seguridad, otros dispositivos y todo lo que el navegador conserve fuera del almacenamiento del origen son asuntos distintos, con sus propias reglas de retención. Lo que la aplicación puede mostrar es más limitado: las bases de datos de la cuenta anterior ya no aparecen en la vista de almacenamiento del navegador y la siguiente cuenta empieza desde un estado vacío. Dilo con claridad en los manuales internos y en cualquier texto de privacidad que se muestre a los usuarios.
El cambio de cuenta añade una regla: termina la eliminación, o delimita los datos, antes de que la siguiente cuenta lea nada. Abrir una base de datos con el nombre de la cuenta nueva es seguro por construcción, mientras que reutilizar una base de datos compartida para varias cuentas obliga a que una purga por clave de cuenta termine primero. Prefiere bases de datos separadas por cuenta cuando las cuentas son independientes para el usuario, porque eliminar una base de datos es más sencillo de verificar que filtrar registros por clave. Para el lado de los service workers de esta misma limpieza, consulta Ciclo de vida y privacidad de la caché del service worker.
Revisa el ciclo de vida entre contextos del navegador
Un ciclo de vida inspira más confianza cuando puedes partir de un estado vacío conocido y ver cada etapa. Dos contextos del navegador que no comparten almacenamiento te lo permiten: uno hace de cuenta con sesión iniciada, el otro hace de usuario siguiente o de segunda cuenta, y ninguno puede ver el IndexedDB del otro. La guía sobre aislamiento de navegador para varias cuentas cubre el lado de separación de cuentas de esta configuración.
BotBrowser documenta que cada BrowserContext creado con browser.newContext() tiene su propio almacenamiento, sus cookies y su estado de sesión, de modo que quien revisa puede iniciar cada recorrido de cuenta desde un estado de IndexedDB separado y verificar el comportamiento de limpieza por contexto. BotBrowser no puede administrar, migrar ni purgar el esquema ni los registros de IndexedDB de una aplicación web, y no puede hacer que la lógica de actualización, cuota o cierre de sesión de una aplicación sea correcta; eso sigue siendo código de la aplicación. La documentación de aislamiento de varias cuentas describe el límite entre contextos en el que se apoya esta revisión.
Usa las herramientas para desarrolladores del navegador como instrumento compartido. En los navegadores basados en Chromium, el panel Application lista las bases de datos de IndexedDB, sus almacenes de objetos y sus registros, y otros navegadores ofrecen una vista de almacenamiento similar. Actualizar esa vista después de cada paso convierte una afirmación como «al cerrar sesión se eliminaron los datos» en algo que una segunda persona puede observar.
Mantén la revisión centrada en tu propia aplicación. Usa cuentas de prueba y registros sintéticos que hayas creado tú, y no apliques estos pasos a datos que haya almacenado otro sitio.
Ejecuta las comprobaciones del ciclo de vida
Ejecuta estas comprobaciones sobre una compilación de la aplicación y anota si cada una pasa o falla.
- Apertura y actualización: la base de datos se abre con una versión explícita, y las llamadas a
createObjectStoreycreateIndexaparecen solo dentro deupgradeneeded. Aumenta la versión y recarga. Pasa si el almacén nuevo aparece enApplication > IndexedDBy los registros existentes siguen siendo legibles. Falla si existe una llamada de esquema fuera del controlador de actualización, o si el almacén nuevo falta o los registros existentes no se pueden leer tras la recarga. - Segunda pestaña: abre la aplicación en dos pestañas y luego aumenta la versión en la segunda. Pasa si la primera pestaña cierra su conexión en
versionchange, muestra un mensaje de recarga y la segunda pestaña termina la actualización. Falla si la segunda pestaña se queda en un estado de carga o informa deblockedsin ningún aviso. - Revisión de almacenes: para cada almacén de objetos, la revisión registra un responsable, una regla de retención y la respuesta ante una cuota superada y ante un desalojo. Borra los datos del origen en las herramientas para desarrolladores y vuelve a abrir la aplicación. Pasa si cada almacén tiene las tres entradas y la aplicación detecta el estado vacío y se recupera o muestra su mensaje documentado. Falla si una entrada está en blanco o la aplicación sigue funcionando sobre una base de datos vacía sin darse cuenta.
- Limpieza al cerrar sesión: cierra sesión y actualiza
Application > IndexedDB. Pasa si no queda ninguna base de datos de la cuenta anterior y la siguiente cuenta empieza vacía. Falla si todavía aparece una base de datos de la cuenta anterior. - Eliminación bloqueada: mantén abierta una segunda pestaña con la cuenta antigua y cierra sesión. Pasa si el flujo termina en un estado definido, como un aviso de purga pendiente y un reintento en el siguiente arranque, dentro de la espera que eligió la aplicación. Falla si la pantalla de cierre de sesión espera sin fin o si los datos anteriores se pueden leer en el siguiente arranque.
- Contextos separados: inicia dos contextos del navegador. Confirma que
Application > IndexedDBestá vacío en ambos contextos. Escribe un registro marcador en el primero y luego abre la misma dirección en el segundo. Pasa si el marcador no está en el IndexedDB del segundo contexto. Falla si aparece.
Repite las comprobaciones después de un cambio de esquema, de una versión que toque el código de almacenamiento o de una actualización mayor del navegador, y conserva el último registro que pasó hasta que la nueva ejecución pase.
Fuentes
Artículos Relacionados
Lleva BotBrowser de la investigación a producción
Usa estas guías para entender el modelo y después avanzar hacia validación multiplataforma, contextos aislados y despliegue de navegador preparado para escalar.