QuotaExceededError и управление квотой

Назначение ошибки QuotaExceededError

Ошибка QuotaExceededError возникает в IndexedDB в тех случаях, когда браузер не может выделить дополнительное пространство для хранения данных. Поскольку Dexie.js является надстройкой над IndexedDB, данная ошибка автоматически передаётся в приложение через механизмы обработки исключений библиотеки.

На практике QuotaExceededError появляется при:

  • сохранении слишком большого объёма данных;
  • накоплении большого количества записей в базе;
  • хранении крупных бинарных объектов;
  • исчерпании лимита локального хранилища браузера;
  • нехватке свободного места на устройстве пользователя;
  • ограничениях приватного режима браузера;
  • превышении квот конкретного происхождения (origin).

Типичный пример:

try {
    await db.files.add({
        name: "video.mp4",
        data: hugeBlob
    });
} catch (error) {
    if (error.name === "QuotaExceededError") {
        console.error("Недостаточно места для сохранения");
    }
}

В Dexie ошибка также может быть перехвачена через собственную систему исключений:

db.files.add(file)
    .catch(Dexie.QuotaExceededError, error => {
        console.error("Квота исчерпана");
    });

Такой способ делает код более читаемым и позволяет обрабатывать конкретный тип ошибки отдельно от остальных.


Как браузеры распределяют дисковое пространство

IndexedDB не предоставляет бесконечное хранилище. Каждый браузер самостоятельно управляет доступным объёмом памяти.

Существуют несколько уровней ограничений:

  1. Лимит на одно происхождение (origin).
  2. Общий лимит браузера.
  3. Ограничения файловой системы устройства.
  4. Политики очистки неиспользуемых данных.

Например, современные браузеры могут выделять:

  • десятки мегабайт автоматически;
  • сотни мегабайт после активного использования сайта;
  • гигабайты при наличии свободного места на устройстве.

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

Главное правило проектирования базы данных в Dexie.js заключается в том, что хранилище всегда следует считать ограниченным ресурсом.


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

Chromium

Браузеры на основе Chromium используют динамическое квотирование.

Размер доступного пространства зависит от:

  • общего объёма диска;
  • свободного места;
  • активности сайта;
  • политики хранения браузера.

Хранилища IndexedDB, Cache API и другие механизмы хранения объединяются в общую систему квот.

Firefox

Firefox использует собственную систему управления локальными данными.

Особенности:

  • отдельные ограничения для происхождений;
  • автоматическая очистка данных при нехватке места;
  • поддержка постоянного хранения.

Safari

Safari традиционно отличается наиболее жёсткими ограничениями.

Особенно это заметно:

  • на iOS;
  • в приватном режиме;
  • при длительном отсутствии активности сайта.

Из-за этого приложения на Dexie.js должны особенно тщательно контролировать объём сохраняемой информации.


Проверка доступного пространства

Современные браузеры поддерживают API оценки используемого хранилища.

Для этого используется объект navigator.storage.

const estimate = await navigator.storage.estimate();

console.log("Использовано:", estimate.usage);
console.log("Квота:", estimate.quota);

Результат может выглядеть следующим образом:

{
    usage: 52428800,
    quota: 2147483648
}

Где:

  • usage — фактически занятое место;
  • quota — максимально доступный объём.

На основе этих данных можно заранее отслеживать приближение к лимиту.


Мониторинг заполнения хранилища

Полезной практикой является периодическая проверка процента использования квоты.

Пример:

async function getStorageUsagePercent() {
    const { usage, quota } =
        await navigator.storage.estimate();

    return (usage / quota) * 100;
}

Использование:

const percent = await getStorageUsagePercent();

if (percent > 80) {
    console.warn("Хранилище почти заполнено");
}

Такой подход позволяет начинать очистку данных ещё до появления QuotaExceededError.


Хранение больших объектов

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

Примеры:

  • фотографии;
  • видеозаписи;
  • PDF-файлы;
  • архивы;
  • аудиофайлы.

Нередко приложение сохраняет объекты размером десятки мегабайт:

await db.files.add({
    fileName: file.name,
    blob: file
});

Несколько сотен подобных записей способны быстро исчерпать доступную квоту.


Сжатие данных перед сохранением

Эффективным способом экономии пространства является компрессия.

Текстовые данные хорошо поддаются сжатию:

const compressed = compress(JSON.stringify(data));

await db.cache.put({
    id: 1,
    payload: compressed
});

Преимущества:

  • уменьшение объёма базы;
  • снижение риска достижения лимита;
  • сокращение времени резервного копирования.

Особенно заметен эффект при хранении больших JSON-структур.


Удаление устаревших записей

Локальная база данных часто содержит временную информацию.

Примеры:

  • кэш запросов;
  • временные результаты вычислений;
  • устаревшие версии документов;
  • старые журналы событий.

Регулярная очистка предотвращает бесконтрольный рост базы.

Пример:

await db.logs
    .where("createdAt")
    .below(Date.now() - 30 * 24 * 60 * 60 * 1000)
    .delete();

Здесь удаляются записи старше 30 дней.


Политика TTL

TTL (Time To Live) позволяет автоматически ограничивать срок хранения данных.

Структура записи:

{
    id: 1,
    value: data,
    expiresAt: Date.now() + 86400000
}

Очистка:

await db.cache
    .where("expiresAt")
    .below(Date.now())
    .delete();

Подобный механизм особенно полезен для:

  • кэшей API;
  • временных токенов;
  • промежуточных вычислений.

Ограничение размера кэша

Кэш без ограничений почти гарантированно приводит к переполнению хранилища.

Распространённый подход — ограничение числа записей.

Например:

const count = await db.cache.count();

if (count > 10000) {
    const oldest = await db.cache
        .orderBy("createdAt")
        .limit(1000)
        .toArray();

    const ids = oldest.map(x => x.id);

    await db.cache.bulkDelete(ids);
}

Таким образом объём данных остаётся контролируемым.


Стратегия LRU

LRU (Least Recently Used) удаляет данные, которые использовались реже всего.

Структура записи:

{
    id: 1,
    value: data,
    lastAccess: Date.now()
}

При чтении:

await db.cache.update(id, {
    lastAccess: Date.now()
});

При очистке:

const obsolete = await db.cache
    .orderBy("lastAccess")
    .limit(500)
    .toArray();

После этого записи удаляются.

Такой механизм широко применяется в офлайн-приложениях и системах локального кэширования.


Частичная загрузка данных

Большие наборы данных не всегда необходимо хранить полностью.

Неэффективный подход:

await db.products.bulkPut(allProducts);

Если каталог содержит сотни тысяч товаров, база быстро разрастётся.

Более рационально сохранять:

  • популярные записи;
  • недавно просмотренные элементы;
  • активные документы;
  • результаты последних запросов.

Разделение данных по уровням важности

Информация может быть классифицирована по критичности.

Постоянные данные

Хранятся максимально долго:

users
settings
documents

Кэшируемые данные

Могут быть удалены при необходимости:

apiCache
imagesCache
searchResults

При нехватке места сначала очищаются именно второстепенные данные.


Получение постоянного хранилища

Некоторые браузеры позволяют запросить постоянное хранилище.

Проверка:

const persistent =
    await navigator.storage.persisted();

Запрос:

const granted =
    await navigator.storage.persist();

Если браузер разрешит постоянное хранение, вероятность автоматического удаления данных существенно снижается.


Реакция на QuotaExceededError

Обработка ошибки должна сопровождаться освобождением пространства.

Пример:

try {
    await db.files.add(fileData);
}
catch (error) {
    if (error.name === "QuotaExceededError") {

        await db.cache.clear();

        await db.files.add(fileData);
    }
}

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


Централизованная обработка ошибок

В крупных проектах удобно создать единый механизм обработки.

async function saveWithQuotaControl(action) {
    try {
        return await action();
    }
    catch (error) {

        if (error.name === "QuotaExceededError") {

            await cleanupStorage();

            return action();
        }

        throw error;
    }
}

Использование:

await saveWithQuotaControl(() =>
    db.files.add(file)
);

Такой подход уменьшает дублирование кода.


Анализ размеров таблиц

При работе с большими базами важно понимать, какие таблицы занимают основное пространство.

Можно выполнять приблизительную оценку:

const records = await db.logs.count();

console.log(records);

Либо вычислять средний размер объектов:

const data = await db.logs.limit(100).toArray();

const avgSize =
    JSON.stringify(data).length / data.length;

Подобная статистика помогает выявлять источники роста базы.


Архивирование данных

Вместо удаления информации иногда применяется архивирование.

Схема работы:

  1. Старые записи экспортируются.
  2. Сохраняются на сервере.
  3. Удаляются из IndexedDB.

Пример:

const oldRecords =
    await db.logs
        .where("date")
        .below(oldDate)
        .toArray();

После передачи на сервер выполняется:

await db.logs
    .where("date")
    .below(oldDate)
    .delete();

Такой подход особенно полезен для журналов активности и истории операций.


Предотвращение роста базы данных

Наиболее эффективная стратегия заключается не в обработке QuotaExceededError, а в предотвращении её появления.

Основные методы:

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

При грамотном управлении жизненным циклом данных даже крупные приложения на Dexie.js способны годами работать без возникновения QuotaExceededError, сохраняя высокую производительность и предсказуемое потребление дискового пространства.