Зависимости и перевычисление запроса

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

В основе механизма лежит анализ того, какие таблицы и индексы были затронуты во время выполнения запроса. Каждый запрос, выполненный в реактивном контексте (например, через liveQuery), регистрирует:

  • используемые таблицы;
  • диапазоны индексов;
  • ключи, участвующие в выборке;
  • операции чтения (get, where, toArray, count и др.).

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

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

Реактивный контекст выполнения

Запрос становится реактивным только внутри специального контекста выполнения. В этом режиме Dexie временно переключает внутренний трекер зависимостей, который:

  1. Активируется перед выполнением запроса.
  2. Регистрирует все обращения к IndexedDB.
  3. Завершает сбор зависимостей после завершения асинхронной операции.

В этот момент формируется подписка вида:

  • таблица → операции чтения → диапазоны ключей → наблюдатели

Такая структура позволяет минимизировать пересчёты: при изменении данных Dexie может определить, какие именно подписки затронуты.

Механизм обнаружения изменений

При выполнении операций модификации данных (put, add, update, delete) Dexie инициирует процесс инвалидации зависимых запросов.

Каждая операция проходит через внутренний слой транзакционного контроля, где фиксируются:

  • имя таблицы;
  • изменённые ключи;
  • диапазон затронутых индексов (если применимо);
  • тип операции (вставка, обновление, удаление).

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

Перевычисление результата запроса

После инвалидации Dexie запускает процесс повторного выполнения запроса. Это не мгновенная операция на каждое изменение, а батчируемый процесс:

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

Псевдопроцесс выглядит следующим образом:

  1. Получение списка затронутых подписок.
  2. Группировка подписок по таблицам и типам запросов.
  3. Дедупликация пересчётов.
  4. Повторное выполнение оригинального запроса в реактивном контексте.
  5. Сравнение результата с предыдущим значением.
  6. Уведомление подписчиков только при изменении данных.

Оптимизация повторных вычислений

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

1. Кэширование структуры запроса

Сам запрос (его функция) сохраняется, а не только результат. Это позволяет повторно использовать логику без повторного анализа кода.

2. Дифференциальное обновление

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

3. Слияние изменений

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

4. Игнорирование нерелевантных изменений

Если изменение затрагивает таблицу, которая не входит в зависимости запроса, пересчёт не запускается.

Типы зависимостей

Зависимости в Dexie можно условно разделить на несколько категорий:

Табличные зависимости

Самый базовый уровень. Запрос зависит от конкретной таблицы:

  • чтение всей таблицы;
  • фильтрация по полям без индекса;
  • агрегации (count, toArray).

Любое изменение в таблице приводит к потенциальной инвалидации.

Индексные зависимости

Если запрос использует where('field') или диапазоны индексов, зависимость становится более точной.

Например:

db.users.where('age').between(18, 30)

В этом случае пересчёт запускается только при изменениях, затрагивающих индекс age.

Ключевые зависимости

При использовании get(primaryKey) или прямого доступа по ключу формируется точечная зависимость. Это наиболее эффективный тип наблюдения, так как затрагивается только один объект.

Пересечение диапазонов и логика инвалидации

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

Если запрос использует диапазон:

  • [10, 20]

а изменение затрагивает ключ:

  • 15

то пересечение считается положительным, и запрос инвалидируется.

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

Внутренне это реализуется через абстракцию диапазонных интервалов, где каждая зависимость хранит:

  • lower bound;
  • upper bound;
  • тип включения границ.

Асинхронная модель перевычисления

Пересчёт запросов выполняется асинхронно, что позволяет избежать блокировки основного потока. Dexie использует очередь задач, в которой:

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

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

Поведение при конкурентных изменениях

Если в процессе пересчёта происходят новые изменения данных, Dexie применяет стратегию:

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

Таким образом предотвращается ситуация, при которой подписчики получают неконсистентное состояние.

Стабилизация результата

Для предотвращения «дребезга» обновлений Dexie сравнивает предыдущий и новый результат запроса.

Если данные не изменились на уровне структуры и значений, уведомление подписчиков не происходит. Сравнение может включать:

  • поверхностную проверку ссылок;
  • глубокое сравнение (в зависимости от конфигурации и типа данных);
  • проверку длины массивов и ключей объектов.

Связь с liveQuery

Механизм зависимостей тесно связан с liveQuery, который является высокоуровневым API реактивности. При использовании liveQuery:

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

Это позволяет строить реактивные цепочки, где изменение данных в IndexedDB автоматически приводит к обновлению UI или других слоёв приложения.

Ограничения модели зависимостей

Несмотря на гибкость, модель имеет ряд ограничений:

  • невозможность отслеживания вычисляемых зависимостей внутри JavaScript-логики;
  • отсутствие точного field-level tracking без индексов;
  • пересчёт всего запроса при сложных агрегациях;
  • потенциальные лишние обновления при широких диапазонах условий.

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

Поведение при сложных запросах

В случае составных запросов, включающих несколько условий (and, or, chained queries), Dexie объединяет зависимости всех подзапросов.

Примерно это выглядит как объединённый граф:

  • таблица A → индекс x
  • таблица A → индекс y
  • таблица B → primary key

Любое изменение в любой из этих точек может привести к пересчёту всего составного запроса.

Инвалидация транзакционного уровня

Особое внимание уделяется транзакциям. Изменения внутри одной транзакции:

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

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

Итоговая модель поведения

Система зависимостей и перевычисления запросов в Dexie представляет собой комбинацию:

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

Такая архитектура обеспечивает предсказуемую реактивность без необходимости ручного управления подписками или сложного кэширования состояния.