Ограничения и подводные камни liveQuery

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

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

Транзакционные границы и момент обновления результата

Пересчёт liveQuery происходит после завершения транзакции записи. Это означает:

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

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

Дополнительно стоит учитывать, что асинхронные цепочки транзакций могут создавать каскадные пересчёты, особенно при последовательных bulkPut, update и delete.

Ограничения детекции изменений

liveQuery реагирует только на изменения, проходящие через Dexie API в текущем контексте выполнения:

  • изменения через table.add, put, delete, bulkPut и т.д. отслеживаются;
  • прямое изменение IndexedDB вне Dexie не гарантирует реактивность;
  • изменения из других вкладок браузера не всегда автоматически синхронизируются.

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

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

Производительность и эффект полного пересчёта

Каждый триггер liveQuery запускает:

  • повторное выполнение всей функции запроса;
  • повторное чтение из IndexedDB;
  • повторную фильтрацию, сортировку и агрегацию.

Это означает, что сложные запросы становятся узким местом:

  • filter на больших коллекциях выполняется в памяти;
  • toArray() на крупных выборках может блокировать поток;
  • цепочки map/filter/reduce не кэшируются между обновлениями.

При частых изменениях данных возникает эффект реактивного штормового пересчёта, когда одно изменение вызывает каскад повторных вычислений.

Нестабильные запросы и зависимость от внешнего состояния

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

  • использование внешних переменных;
  • чтение Date.now() внутри запроса;
  • генерация случайных значений;
  • зависимость от состояния UI или глобальных singleton-объектов.

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

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

Асинхронные операции внутри liveQuery

Хотя liveQuery поддерживает async функцию, его модель остаётся синхронной по логике пересчёта:

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

Это создаёт эффект гонки:

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

Побочные эффекты и нарушение чистоты запроса

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

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

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

Обработка ошибок и деградация подписки

Ошибки внутри liveQuery не ведут к краху всей системы, но имеют специфическое поведение:

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

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

Особую проблему создают:

  • ошибки индексов (missing index, invalid keyPath);
  • изменения схемы базы без миграции;
  • попытки чтения несуществующих таблиц.

Жизненный цикл подписок и утечки памяти

Каждый вызов liveQuery(...).subscribe(...) создаёт активную подписку. При отсутствии явного завершения:

  • сохраняется ссылка на observer;
  • сохраняется контекст выполнения запроса;
  • продолжают работать внутренние слушатели Dexie hooks.

В долгоживущих приложениях это приводит к накоплению подписок, особенно при:

  • частом монтировании/размонтировании компонентов;
  • динамическом создании реактивных запросов;
  • отсутствии unsubscribe.

Эффект проявляется как постепенное снижение производительности без явных ошибок.

Дублирование вычислений и отсутствие глобального кэша

Каждая подписка liveQuery выполняет запрос независимо. Это означает:

  • одинаковые запросы в разных частях приложения не шарят результат;
  • нет встроенного дедуплицированного кэша;
  • повторные вычисления происходят параллельно.

При архитектуре с множеством компонентов это может приводить к N-кратному выполнению одного и того же тяжёлого запроса.

Индексы и их влияние на реактивность

Хотя Dexie эффективно использует индексы, liveQuery не делает запрос “реактивным на уровне индекса”. Это означает:

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

Особая проблема возникает при фильтрации через .filter(), так как она выполняется после загрузки данных и полностью повторяется при каждом триггере.

Сортировка, лимиты и пагинация

Комбинации orderBy, limit, offset внутри liveQuery создают скрытые сложности:

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

Типичный эффект — “прыгающие страницы”, когда элементы смещаются между обновлениями.

Мульти-вкладочная синхронизация и расхождение состояний

В многовкладочной среде liveQuery не гарантирует мгновенную консистентность:

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

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

Версионирование схемы и реактивные сбои

При обновлении схемы базы данных:

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

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

Поведенческие паттерны, снижающие нестабильность

Снижение проблем liveQuery обычно сводится к архитектурным ограничениям:

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

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