Идентичность

Жизненный цикл IndexedDB: версии, квота и очистка

Откройте и версионируйте IndexedDB без ошибок, обновляйте схему без блокировки вкладок, заранее учитывайте квоту и вытеснение и удаляйте данные при выходе.

Документация

Нужна структурированная документация по теме Идентичность?

Эта статья относится к редакционной библиотеке. Для пошаговой настройки, справки и постоянных обновлений переходите сразу в соответствующий раздел docs.

Откройте базу данных с явной версией

У базы данных IndexedDB есть жизненный цикл из четырёх этапов, которые код приложения должен обрабатывать осознанно: открытие с указанием версии, обновление схемы, работа в пределах квоты, которой управляет браузер, и удаление, когда данные больше не должны существовать. На каждом этапе есть событие или API, сообщающее о происходящем, и у каждого есть исход при сбое, который виден пользователю. Надёжное приложение открывает базу с явной версией, меняет схему только на шаге обновления, закрывает своё соединение, когда другая вкладка запрашивает более новую версию, фиксирует, что происходит при нехватке места, и удаляет базы предыдущего аккаунта при выходе, получая ограниченный по времени результат, если удаление заблокировано.

Всё дальнейшее относится к данным, которые хранит ваше собственное приложение. Чтение или изучение данных, сохранённых другим сайтом, это другая задача, а имена баз и хранилищ объектов в примерах выдуманы и не взяты с реального сайта. Чтобы понять, как IndexedDB соотносится с cookie и веб-хранилищем, прочитайте Cookie, localStorage и IndexedDB: где хранить состояние.

Жизненный цикл базы IndexedDB: открытие с явной версией, обновление только в upgradeneeded, учёт квоты и вытеснения, удаление при выходе и проверка в инструментах разработчика

Точка входа это indexedDB.open(name, version). Вызов возвращает запрос на открытие, а не саму базу. Соединение приходит в событии success запроса, а любое создание или изменение схемы происходит в событии upgradeneeded, которое срабатывает первым, если запрошенная версия выше сохранённой. Если базы ещё нет, она создаётся с запрошенной версией, и upgradeneeded выполняется один раз, чтобы построить начальные хранилища объектов. Эту последовательность определяет спецификация Indexed Database API от W3C, а руководство MDN по использованию IndexedDB разбирает её на примерах.

Передавайте версию явно и держите её именованной константой в коде, который отвечает за схему. Если аргумент версии опущен, браузер открывает существующую базу на текущей версии или создаёт новую на первой версии, поэтому код не может запросить обновление намеренно. Целое число, которое команда увеличивает при каждом изменении схемы, делает историю проверяемой: каждый номер соответствует известному набору хранилищ объектов и индексов, а запрос на слияние, меняющий схему, меняет и константу.

Версия ниже сохранённой тоже даёт определённый исход. Запрос на открытие завершается ошибкой VersionError. Так бывает, когда пользователь оставил открытой старую вкладку, а более новая уже обновила базу, или когда после выхода новой версии выполняется старая кешированная копия кода приложения. Считайте эту ошибку состоянием приложения с сообщением и действием перезагрузки и не повторяйте попытки в цикле с угаданным номером версии.

Обрабатывайте исходы запроса в одном месте. Событие success передаёт соединение, error сообщает о сбое, например о VersionError или о проблеме хранения, от которой браузер не может оправиться, а blocked сообщает, что другие соединения всё ещё держат базу открытой, пока обновление ждёт. Оберните вызов открытия в небольшую функцию, возвращающую промис, подключите обработчик versionchange к соединению до того, как вернуть его, и пусть остальное приложение запрашивает базу у этой функции, а не открывает свою. Один владелец открытия и закрытия делает жизненный цикл соединения понятным.

Называйте базы и хранилища объектов по тому, чем они владеют. Используйте стабильное имя базы, выбранное приложением, а если один источник обслуживает несколько аккаунтов, включайте в имя непрозрачный локальный ключ аккаунта вместо адреса электронной почты или других персональных данных. Имя видно в инструментах разработчика и часто попадает в обращения в поддержку, и именно оно позже понадобится сценарию выхода, чтобы найти и удалить данные одного аккаунта.

Обновляйте схему, не блокируя другие вкладки

Изменять схему разрешено только внутри обработчика upgradeneeded. Во время этого события соединение держит особую транзакцию смены версии, и только внутри неё код может вызывать createObjectStore, deleteObjectStore, createIndex или deleteIndex. Вне её эти вызовы выбрасывают ошибку. Спецификация W3C задаёт у события значения oldVersion и newVersion, благодаря которым один обработчик может довести базу вперёд с любой предыдущей версии.

Пишите обработчик как последовательность шагов, привязанных к старой версии: если старая версия ниже первой цели, создайте начальные хранилища; если ниже второй, добавьте новый индекс; и так далее. Пользователь может прийти с любой предыдущей версии, поэтому каждый шаг должен выполняться корректно по порядку и сам по себе. Ограничивайте обработчик работой с IndexedDB. Транзакция обновления завершается, когда у неё не остаётся ожидающих запросов, поэтому ожидание сетевого вызова или постороннего промиса внутри обработчика может завершить транзакцию до следующего шага. Загружайте удалённые данные после успешного открытия, а не во время обновления.

Перенос существующих записей в новую форму это самая рискованная часть. Выполняйте преобразование в той же транзакции обновления, обходя старое хранилище курсором, чтобы шаг либо зафиксировался целиком, либо не зафиксировался совсем. Если обработчик выбрасывает исключение или вызывает abort() для транзакции, версия остаётся прежней, а запрос на открытие завершается ошибкой AbortError, так что прежняя схема остаётся нетронутой. Делайте каждую миграцию небольшой и проверяйте её на базе, созданной на каждой старой версии, которую приложение ещё поддерживает, а не только на последней.

Вторая вкладка это место, где обновления обычно ломаются. IndexedDB позволяет базе сменить версию, только когда не открыто ни одного более старого соединения. Когда одна вкладка запрашивает более высокую версию, браузер вызывает versionchange на соединениях, которые ещё держат другие вкладки. Руководство MDN по использованию IndexedDB указывает, что обработчик должен закрыть соединение, чтобы другая страница могла выполнить обновление. Если он этого не делает, новый запрос на открытие вызывает blocked и остаётся в ожидании, обновление не выполняется, а пользователь видит, что новая вкладка зависла в состоянии загрузки.

Хороший обработчик делает две вещи. Он сразу вызывает db.close(), а затем сообщает пользователю о случившемся, например показывает, что приложение обновилось в другой вкладке, и предлагает перезагрузить страницу. Закрытие освобождает базу для вкладки, которая обновляет схему, а сообщение объясняет, почему эта вкладка перестала работать. Не продолжайте пользоваться соединением после закрытия, потому что новые транзакции на нём выбрасывают ошибку. То же событие срабатывает, когда вкладка вызывает deleteDatabase, поэтому один обработчик покрывает и обновления, и удаления.

Вкладка, запросившая обновление, тоже должна обработать blocked. Покажите короткое уведомление о том, что другое окно всё ещё открыто на более старой версии, оставьте запрос на открытие в ожидании и дайте обновлению завершиться, как только другое соединение закроется. Если приложение также запускает сервис-воркер или общий воркер, открывающий базу, включите его в проверку. Воркер это ещё одно соединение, которое может держать обновление в ожидании, и ему нужна та же обработка versionchange.

Заранее продумайте квоту и вытеснение

Браузеры делят ограниченный объём дискового пространства между источниками, и данные IndexedDB живут в рамках этого бюджета. Стандарт WHATWG о хранилище описывает хранение источника как «по возможности» по умолчанию: браузер может удалить данные при нехватке места, не спрашивая. Страница MDN о квотах и критериях вытеснения поясняет, что лимиты и порядок вытеснения зависят от браузера. Цифры ёмкости различаются между браузерами, устройствами и версиями, поэтому приложению не следует зависеть от числа, которое оно не может проверить.

Два исхода требуют определённой реакции приложения. Первый это запись, которая не помещается. Когда транзакция не может зафиксироваться из-за нехватки места, сбой сообщается как QuotaExceededError, а транзакция прерывается. Слушайте abort и error на транзакции, а не только на отдельных запросах, и заранее решите, что увидит пользователь. Разумные реакции включают удаление наименее ценных кешированных записей, приостановку синхронизации или просьбу к пользователю освободить место, в зависимости от вида данных.

Второй исход это данные, которых просто нет. Источник с хранением «по возможности» может быть вытеснен, а браузеры, которые вытесняют данные, обычно удаляют данные источника целиком, а не по нескольку записей. Следующий визит находит пустую базу, и приложение снова открывает её с первой версией. Проектируйте с учётом этого случая. Держите запись-маркер или хранилище метаданных, чтобы код запуска отличал свежую установку от вытесненной, и восстанавливайте данные с сервера там, где у них есть серверная копия.

Вызов navigator.storage.estimate() возвращает приблизительные значения использования и квоты, помогающие решить, когда сокращать кеш, а navigator.storage.persist() просит браузер считать данные источника постоянными. Оба вызова дают оценки и запросы в том виде, как их описывает стандарт WHATWG о хранилище. Браузер может отказать в постоянстве, может спросить пользователя или может решить без запроса. Фиксируйте в проверке результат запроса, а не считайте, что он удовлетворён, и сохраняйте тот же путь восстановления на случай, если он не был удовлетворён.

Записывайте решения для каждого хранилища объектов в небольшую таблицу рядом с кодом схемы. Для каждого хранилища укажите владельца, правило срока хранения и то, что делает приложение, когда квота превышена или данные вытеснены. Хранилище черновиков, которые пользователь не синхронизировал, требует предупреждения и пути экспорта. Хранилище кешированных ответов сервера требует лишь восстановления. Хранилищу настроек может понадобиться значение по умолчанию. Правила срока хранения относятся к той же таблице: кеш недавних элементов можно очищать при запуске, обходя курсором индекс по полю с отметкой времени и удаляя записи старше указанного возраста небольшими пакетами, чтобы каждая транзакция оставалась короткой.

Удаляйте данные приложения при выходе и смене аккаунта

Выход это решение о данных, а не только о сеансе. Когда человек выходит из аккаунта на общем компьютере или один аккаунт заменяет другой в том же профиле браузера, данные IndexedDB предыдущего аккаунта остаются на диске под тем же источником, пока приложение их не удалит. Сценарию очистки нужны чёткий перечень того, что удалять, способ это найти и определённый исход на случай, когда удаление не может завершиться.

Ведите реестр имён баз, которые создаёт приложение, например короткий список в коде или запись метаданных, вместо того чтобы полагаться на обнаружение. Где доступно, indexedDB.databases() может перечислить базы источника и полезен на шаге проверки, но поддержка в браузерах со временем менялась, поэтому прочитайте примечания о совместимости, прежде чем делать его единственным источником. С реестром выход превращается в цикл: закройте собственные соединения этой вкладки, затем вызовите indexedDB.deleteDatabase(name) для каждого имени, принадлежащего уходящему аккаунту.

Метод возвращает запрос, как и open. Его справочная страница сообщает, что удаление вызывает versionchange на открытых соединениях и, если какие-то остаются открытыми, вызывает blocked на запросе, а само удаление ждёт их закрытия. Поэтому обработчик versionchange из раздела об обновлении важен и здесь, и поэтому сценарий выхода, который сначала не закрывает собственное соединение, заблокирует сам себя. Задайте сценарию ограниченный исход: ждите success, а если приходит blocked и не разрешается за короткое время ожидания, выбранное приложением, зафиксируйте, что очистка отложена, сообщите пользователю и повторите попытку при следующем запуске до чтения любых данных предыдущего аккаунта. Не оставляйте на экране выхода бесконечный индикатор ожидания.

Сервер тоже может попросить браузер очистить данные заголовком ответа Clear-Site-Data. Директива "storage" охватывает IndexedDB вместе с другим хранилищем источника, таким как localStorage и регистрации сервис-воркеров, а значит, очищает больше, чем IndexedDB, и лучше подходит для полного выхода, чем для удаления одного аккаунта из нескольких. Справка по заголовку отмечает, что он учитывается только в защищённых ответах и что поддержка различается между браузерами. Открытые соединения могут задержать или ограничить очистку, поэтому оставьте удаление на стороне приложения, описанное выше, как надёжный путь, а заголовок считайте дополнительным шагом.

Ни один из этих инструментов не доказывает, что не осталось ни одной копии данных пользователя. Серверные записи, резервные копии, другие устройства и всё, что браузер хранит вне хранилища источника, это отдельные вопросы со своими правилами срока хранения. Приложение может показать более узкую вещь: базы предыдущего аккаунта больше не отображаются в представлении хранилища браузера, а следующий аккаунт начинает с пустого состояния. Говорите об этом прямо во внутренних инструкциях и в любом тексте о конфиденциальности, который видят пользователи.

Смена аккаунта добавляет одно правило: завершите удаление или разделите данные до того, как следующий аккаунт что-либо прочитает. Открытие базы, названной по новому аккаунту, безопасно по построению, тогда как повторное использование одной общей базы для нескольких аккаунтов означает, что очистка по ключу аккаунта должна завершиться первой. Предпочитайте отдельные базы для каждого аккаунта, когда аккаунты в представлении пользователя независимы, потому что удаление одной базы проще проверить, чем фильтрацию записей по ключу. О стороне сервис-воркеров в той же очистке читайте в материале Жизненный цикл и приватность кеша сервис-воркера.

Проверьте жизненный цикл в разных контекстах браузера

Жизненному циклу проще доверять, когда можно начать с известного пустого состояния и наблюдать каждый этап. Это дают два контекста браузера, не разделяющие хранилище: один играет роль аккаунта с выполненным входом, другой играет следующего пользователя или второй аккаунт, и ни один не видит IndexedDB другого. Руководство по изоляции браузера для нескольких аккаунтов охватывает сторону разделения аккаунтов в такой схеме.

BotBrowser документирует, что у каждого BrowserContext, созданного через browser.newContext(), есть собственное хранилище, свои cookie и своё состояние сеанса, поэтому проверяющий может начинать каждый путь аккаунта с отдельного состояния IndexedDB и проверять поведение очистки в каждом контексте. BotBrowser не управляет схемой и записями IndexedDB веб-приложения, не переносит и не очищает их и не может сделать корректной логику обновления, квоты или выхода приложения; это остаётся кодом приложения. Документация по изоляции нескольких аккаунтов описывает границу контекстов, на которую опирается эта проверка.

Используйте инструменты разработчика браузера как общий инструмент наблюдения. В браузерах на основе Chromium панель Application показывает базы IndexedDB, их хранилища объектов и записи, а другие браузеры предлагают похожее представление хранилища. Обновление этого представления после каждого шага превращает утверждение вроде «выход удалил данные» в то, что может увидеть второй человек.

Держите проверку в рамках собственного приложения. Используйте тестовые аккаунты и синтетические записи, которые создали вы сами, и не направляйте эти шаги на данные, сохранённые другим сайтом.

Выполните проверки жизненного цикла

Выполните эти проверки на сборке приложения и отметьте результат «пройдено» или «не пройдено» для каждой.

  1. Открытие и обновление: база открывается с явной версией, а вызовы createObjectStore и createIndex встречаются только внутри upgradeneeded. Повысьте версию и перезагрузите страницу. Пройдено, если новое хранилище появилось в Application > IndexedDB, а существующие записи читаются. Не пройдено, если вызов схемы найден вне обработчика обновления, либо если нового хранилища нет или существующие записи не читаются после перезагрузки.
  2. Вторая вкладка: откройте приложение в двух вкладках, затем повысьте версию во второй. Пройдено, если первая вкладка закрывает соединение на versionchange, показывает сообщение о перезагрузке, а вторая вкладка завершает обновление. Не пройдено, если вторая вкладка остаётся в состоянии загрузки или сообщает blocked без всякого уведомления.
  3. Проверка хранилищ: для каждого хранилища объектов проверка фиксирует владельца, правило срока хранения и реакцию на превышение квоты и на вытеснение. Очистите данные источника в инструментах разработчика и снова откройте приложение. Пройдено, если у каждого хранилища есть все три записи, а приложение обнаруживает пустое состояние и восстанавливается или показывает описанное сообщение. Не пройдено, если запись пуста или приложение продолжает работать на пустой базе, не заметив этого.
  4. Очистка при выходе: выйдите из аккаунта и обновите Application > IndexedDB. Пройдено, если ни одной базы предыдущего аккаунта не осталось, а следующий аккаунт начинает с пустого состояния. Не пройдено, если база предыдущего аккаунта всё ещё в списке.
  5. Заблокированное удаление: оставьте вторую вкладку открытой со старым аккаунтом и выйдите. Пройдено, если сценарий завершается в определённом состоянии, например уведомлением об отложенной очистке и повтором при следующем запуске, в пределах ожидания, выбранного приложением. Не пройдено, если экран выхода ждёт бесконечно или прежние данные читаются при следующем запуске.
  6. Раздельные контексты: запустите два контекста браузера. Убедитесь, что Application > IndexedDB пуст в обоих контекстах. Запишите запись-маркер в первом, затем откройте тот же адрес во втором. Пройдено, если маркера нет в IndexedDB второго контекста. Не пройдено, если он появился.

Повторяйте проверки после изменения схемы, выпуска, затрагивающего код хранения, или крупного обновления браузера и сохраняйте последнюю успешную запись, пока новый прогон не пройдёт.

Источники

#IndexedDB#Хранилище Браузера#Данные Сайта#Контексты Браузера

Переведите BotBrowser из исследований в продакшн

Используйте эти руководства, чтобы понять модель, а затем перейти к кроссплатформенной валидации, изолированным контекстам и масштабируемому браузерному развертыванию.