Перейти к содержанию

Квоты и вытеснение локальных данных

В предыдущих уроках мы выбирали главы, готовили полные пакеты и сохраняли закладки. Однако приложение не владеет свободным местом устройства. Браузер определяет доступный объём, может отказать в новой записи и управляет жизненным циклом локальных данных. Офлайн-учебник должен учитывать эти условия в своём обычном интерфейсе.

В этом уроке вы добавите к offline-lab сведения о хранении и явный запрос повышенной устойчивости. Разберём, что означают возвращаемые значения, почему предварительная оценка не гарантирует успешную загрузку и какие данные требуют отдельного способа сохранения.

Размер пакета и расход origin

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

Метод navigator.storage.estimate() возвращает приблизительные usage и quota в байтах. Значения могут учитывать сжатие и другие особенности хранения, поэтому это ориентир. Описание estimate в MDN. В интерфейсе называйте число оценкой, а не точным размером папки на диске.

Origin включает схему, имя хоста и порт. Перенос учебника на другой адрес не переносит вместе с ним прежние локальные записи. Два пути одного origin, напротив, не становятся двумя независимыми квотами только из-за разных каталогов. Наш стенд специально имеет свой origin, чтобы не смешивать его данные с другими приложениями сайта.

Читаем доступные сведения

Добавьте внутрь main следующий полный самостоятельный блок:

<section aria-labelledby="storage-title">
  <h2 id="storage-title">Локальные данные учебника</h2>
  <p id="storage-estimate">Оценка ещё не получена.</p>
  <button id="storage-refresh" type="button">Обновить оценку</button>
  <button id="storage-keep" type="button" hidden>Запросить защиту хранения</button>
  <p id="storage-status" role="status" aria-atomic="true"></p>
</section>
<script type="module" src="/assets/storage-status.js"></script>

Создайте полный assets/storage-status.js. Здесь запрос защиты связан с кнопкой, а начальная загрузка только читает состояние:

const estimateLine = document.getElementById("storage-estimate");
const status = document.getElementById("storage-status");
const refresh = document.getElementById("storage-refresh");
const keep = document.getElementById("storage-keep");
const manager = navigator.storage;
const mib = value => (value / 1048576).toFixed(1);

export async function readStorageState() {
  if (typeof manager?.estimate !== "function") return {state: "unsupported"};
  const {usage, quota} = await manager.estimate();
  if (!Number.isFinite(usage) || usage < 0 || !Number.isFinite(quota) || quota <= 0) {
    return {state: "unknown"};
  }
  let persistent = null;
  if (typeof manager.persisted === "function") {
    try { persistent = await manager.persisted(); }
    catch { persistent = null; }
  }
  return {state: "estimated", usage, quota,
          remaining: Math.max(0, quota - usage), persistent};
}

async function showEstimate() {
  refresh.disabled = true;
  try {
    const data = await readStorageState();
    if (data.state !== "estimated") {
      estimateLine.textContent = "Оценка места недоступна; это не означает нулевой расход.";
      return;
    }
    const persistence = data.persistent === true ? "Защита хранения разрешена." :
      data.persistent === false ? "Защита хранения не разрешена." :
      "Состояние защиты неизвестно.";
    estimateLine.textContent = "Занято примерно " + mib(data.usage) + " МиБ из " +
      mib(data.quota) + " МиБ квоты. Оценка остатка: " + mib(data.remaining) +
      " МиБ. " + persistence;
  } catch {
    estimateLine.textContent = "Браузер не предоставил сведения о хранении.";
  } finally { refresh.disabled = false; }
}

keep.hidden = typeof manager?.persist !== "function";
refresh.addEventListener("click", showEstimate);
keep.addEventListener("click", async () => {
  keep.disabled = true;
  try {
    const granted = await manager.persist();
    status.textContent = granted ?
      "Браузер разрешил защиту от вытеснения. Ручное удаление данных остаётся возможным." :
      "Браузер не разрешил защиту. Учебник продолжит работу с обычным хранением.";
    await showEstimate();
  } catch {
    status.textContent = "Запрос защиты не выполнен. Сохранённые копии не удалялись.";
  } finally { keep.disabled = false; }
});
showEstimate();

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

Предположим, учебный ответ содержит usage=1048576 и quota=10485760. Ожидаемые величины — около 1,0 МиБ занято и 9,0 МиБ остатка. Это вымышленные числа для разбора деления, а не измерение ProfessorWeb. МиБ здесь означает деление на 1024 × 1024, а не десятичный мегабайт.

Что разрешает persistent storage

persisted() спрашивает о текущем режиме, а persist() просит изменить его и возвращает логический результат. Браузер может отказать; методы имеют разные назначения. Чтение режима, запрос persistent storage.

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

Не обещайте одинаковый диалог разрешения во всех браузерах. Решение может приниматься автоматически или сопровождаться вопросом. Запрос следует связывать с понятной потребностью читателя и не повторять навязчиво после отказа. Рекомендации web.dev по persistent storage. В нашем учебном интерфейсе читатель сам выбирает кнопку; установка PWA отдельно не выдаётся за полученное разрешение.

Оценка не резервирует место

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

При превышении квоты запись Cache API или IndexedDB может завершиться QuotaExceededError. Данные обычного режима могут быть вытеснены браузером; потеря origin способна затронуть одновременно кеш и базу. Квоты и вытеснение в MDN. Численный лимит одного браузера не стоит превращать в общий договор всех устройств.

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

Автоматическое удаление старой копии перед каждой попыткой опасно: новая загрузка может затем не завершиться. Сначала определите, какие пакеты используются открытыми вкладками и что читатель согласился удалить. Предварительное освобождение места является отдельным действием, а не скрытым побочным эффектом кнопки «Обновить».

Пользовательские данные требуют другого отношения

HTML и CSS можно восстановить из опубликованного выпуска. Закладки article_id и heading_id отражают выбор читателя; сервер о них может ничего не знать. Поэтому для важного продолжения полезен явный экспорт и последующий импорт, а при развитии продукта — отдельно спроектированная синхронизация. Сам запрос persistent storage этого не реализует.

Не называйте локальную запись синхронизированной только потому, что она пережила перезагрузку вкладки. И не записывайте её в новый пакет вместе с ресурсами: удаление ненужной копии статьи тогда могло бы уничтожить состояние чтения. Разделение Cache API и pw-reader сохраняет возможность адресного ремонта, хотя не отменяет общей политики origin.

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

Завершённая модель офлайн-учебника

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

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