Отписка и управление жизненным циклом подписки

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

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


Observable и механизм выполнения liveQuery

Функция Dexie.liveQuery() формирует ленивый observable-поток. Вычисление запроса не происходит до момента подписки. При подписке запускается выполнение функции, возвращающей Promise, который преобразуется в поток значений.

import { liveQuery } fr om "dexie";

const observable = liveQuery(async () => {
    return await db.todos.wh ere("done").equals(0).toArray();
});

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


Объект подписки и управление временем жизни

Подписка создаётся через метод subscribe, возвращающий объект управления жизненным циклом:

const subscription = observable.subscribe({
    next: value => console.log(value),
    error: err => console.error(err)
});

Объект подписки содержит как минимум:

  • unsubscribe() — завершение потока
  • внутренний флаг состояния завершения
  • ссылку на активную вычислительную цепочку

После вызова unsubscribe() дальнейшие эмиссии значений прекращаются, а все внутренние ресурсы освобождаются.


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

Отписка в Dexie.js не ограничивается остановкой внешних уведомлений. Она разрывает всю цепочку реактивного пересчёта:

  1. Прерывается активная асинхронная операция чтения IndexedDB
  2. Отменяется подписка на внутренние триггеры изменения таблиц
  3. Освобождается кэш вычисленного результата
  4. Удаляются ссылки на observer-коллбеки

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


AbortSignal и отмена вычислений

Внутренне liveQuery использует механизм отмены через AbortController. Каждый запуск вычисления получает сигнал отмены, позволяющий прерывать запрос при:

  • изменении данных до завершения предыдущего выполнения
  • явной отписке
  • пересоздании подписки при повторной активации
const observable = liveQuery(async (signal) => {
    const data = await db.items.toArray({ signal });
    return data;
});

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


Перезапуск потока и инвалидация состояния

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

С точки зрения жизненного цикла подписки это означает:

  • активный поток переходит в состояние “recompute pending”
  • предыдущий результат остаётся валидным до завершения нового вычисления
  • подписчики получают обновление только после успешного завершения нового запроса

Это предотвращает промежуточные неконсистентные состояния.


Утечки памяти при некорректном управлении подписками

Основной риск связан с удержанием активных подписок при длительном существовании приложения. Поскольку каждая подписка удерживает ссылки на:

  • callback-функции
  • внутренние кэши запроса
  • контекст IndexedDB операций

неосвобождённые подписки формируют цепочки удержания памяти.

Типичные сценарии:

  • создание подписки без сохранения ссылки на subscription
  • повторное создание liveQuery без завершения предыдущего потока
  • отсутствие явного завершения при смене контекста выполнения

Повторные подписки и параллельные потоки

Каждый вызов subscribe() создаёт независимый поток выполнения. Даже при идентичном запросе liveQuery не гарантирует шаринг результата между подписчиками.

const obs = liveQuery(() => db.items.toArray());

const sub1 = obs.subscribe(v => console.log("A", v));
const sub2 = obs.subscribe(v => console.log("B", v));

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

Это означает, что оптимизация через повторное использование observable-объекта не эквивалентна объединению потоков.


Завершение подписки при изменении контекста

В UI-архитектурах подписки часто привязываются к жизненному циклу компонентов. При смене контекста выполнение должно завершаться до создания нового потока.

Сценарий гонки:

  • создаётся подписка A
  • создаётся подписка B до завершения A
  • A продолжает получать обновления параллельно с B

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


Ошибки в потоке и их влияние на жизненный цикл

Если функция liveQuery выбрасывает исключение, поток переходит в состояние ошибки. В этом состоянии:

  • новые значения не эмитятся
  • подписка остаётся активной до явного завершения
  • повторное вычисление не происходит автоматически
const obs = liveQuery(async () => {
    throw new Error("fail");
});

Обработчик error получает управление, но поток не восстанавливается без пересоздания подписки.


Идемпотентность отписки и безопасность повторных вызовов

Метод unsubscribe() проектируется как идемпотентный. Повторные вызовы не изменяют состояние и не приводят к исключениям.

Это важно в условиях неопределённого жизненного цикла, когда:

  • несколько систем пытаются завершить одну подписку
  • cleanup вызывается как в синхронном, так и асинхронном контексте
  • происходит параллельное завершение компонентов

Очистка ресурсов и внутренние очереди Dexie

При завершении подписки происходит очистка:

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

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


Ресурсный контекст и каскадное завершение

Подписки Dexie формируют каскадную структуру:

  • observable
  • внутренний executor liveQuery
  • IndexedDB транзакция
  • набор подписчиков изменений таблиц

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


Многократное создание и пересоздание подписок

При частом пересоздании observable возникает паттерн “подписка-дестрой”:

  • старая подписка завершается
  • создаётся новая с тем же запросом
  • предыдущий кэш становится недействительным

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


Завершение при остановке окружения выполнения

В браузерных средах жизненный цикл подписки часто пересекается с:

  • закрытием вкладки
  • перезагрузкой страницы
  • навигацией SPA

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