Подписка на изменения в хранилище

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

В основе лежит идея отслеживания операций CRUD на уровне таблиц и последующей трансляции этих событий в слой приложения.


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

Dexie.js предоставляет механизм table hooks, позволяющий перехватывать операции до и после их выполнения. Эти хуки работают синхронно с транзакциями и являются фундаментом для построения реактивности.

Основные типы хуков

Каждая таблица поддерживает три базовые точки расширения:

  • creating — перед добавлением записи
  • updating — перед обновлением записи
  • deleting — перед удалением записи
  • reading — при чтении записи из IndexedDB

creating

Хук creating вызывается до фактической вставки объекта в хранилище. Он позволяет модифицировать данные или вычислить поля.

db.users.hook('creating', (primKey, obj, transaction) => {
  obj.createdAt = Date.now();
});

Особенности поведения:

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

updating

Хук updating активируется при частичном обновлении записи через update() или modify().

db.users.hook('updating', (mods, primKey, obj, transaction) => {
  mods.updatedAt = Date.now();
});

Характеристики:

  • mods содержит только изменяемые поля
  • исходный объект доступен для анализа
  • изменение mods влияет на итоговую запись

deleting

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

db.users.hook('deleting', (primKey, obj, transaction) => {
  db.audit.add({
    type: 'delete',
    id: primKey,
    snapshot: obj
  });
});

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

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

reading

Хук чтения применяется при извлечении данных из IndexedDB.

db.users.hook('reading', (obj) => {
  obj.fullName = obj.firstName + ' ' + obj.lastName;
});

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

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

Управление хуками и порядок выполнения

Dexie выполняет хуки в строго определённой последовательности:

  1. creating / updating / deleting (в зависимости от операции)
  2. выполнение транзакции
  3. reading при извлечении данных

Несколько хуков одного типа выполняются последовательно в порядке регистрации.

db.users.hook('creating', fn1);
db.users.hook('creating', fn2);

Здесь fn1 выполнится раньше fn2.

Удаление хуков возможно через unsubscribe:

const hook = db.users.hook('creating').subscribe(callback);
hook.unsubscribe();

Реактивные запросы через liveQuery

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

import { liveQuery } fr om "dexie";

const activeUsers$ = liveQuery(() =>
  db.users.wh ere('active').equals(1).toArray()
);

Механизм работы:

  • выполняется запрос
  • фиксируются затронутые таблицы
  • при изменении этих таблиц запрос пересчитывается
  • подписчики получают новое значение

Подписка на liveQuery

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

const subscription = activeUsers$.subscribe({
  next: (users) => {
    console.log(users);
  },
  error: (err) => {
    console.error(err);
  }
});

Поведение:

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

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

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

subscription.unsubscribe();

Сценарии применения:

  • уничтожение компонентов интерфейса
  • смена маршрута в SPA
  • временные наблюдения за состоянием

Кэширование зависимостей запроса

Dexie анализирует выполнение запроса внутри liveQuery и определяет зависимости:

  • таблицы
  • индексы
  • диапазоны запросов

При изменении любой из зависимостей происходит инвалидация кэша и повторный запуск функции.

liveQuery(() =>
  db.orders.where('status').anyOf(['new', 'pending']).toArray()
);

Любое изменение orders с соответствующим status вызывает пересчёт.


Сочетание хуков и реактивных запросов

Хуки и liveQuery дополняют друг друга, но работают на разных уровнях:

  • хуки — низкоуровневый контроль операций записи
  • liveQuery — реактивное чтение состояния

Пример комбинированного использования:

db.orders.hook('creating', (id, obj) => {
  obj.createdAt = Date.now();
});

const orders$ = liveQuery(() =>
  db.orders.orderBy('createdAt').toArray()
);

Хук формирует данные, реактивный запрос отслеживает их изменения.


Реактивные цепочки и производные данные

Dexie позволяет строить цепочки вычисляемых данных поверх liveQuery.

const totalPrice$ = liveQuery(() =>
  db.orders.toArray().then(orders =>
    orders.reduce((sum, o) => sum + o.price, 0)
  )
);

Любое изменение в orders приводит к пересчёту агрегата.

Такая модель используется для:

  • вычисления метрик
  • агрегированных дашбордов
  • производных состояний интерфейса

Особенности производительности подписок

Реактивные механизмы Dexie оптимизированы под минимизацию лишних пересчётов.

Ключевые принципы:

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

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


Обработка ошибок в реактивных потоках

Ошибки могут возникать на любом этапе выполнения запроса:

  • нарушение индексов
  • удалённые таблицы
  • ошибки пользовательской логики
liveQuery(() => db.users.toArray())
  .subscribe({
    error: (err) => {
      console.error(err);
    }
  });

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


Интеграция с внешними реактивными системами

Результаты liveQuery часто интегрируются с внешними реактивными библиотеками через адаптеры:

  • RxJS Observable
  • React state management
  • Vue reactivity system

Пример с внешним подписчиком:

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

stream.subscribe(data => {
  store.setState({ items: data });
});

Такая схема позволяет использовать Dexie как источник реактивного состояния приложения без прямой привязки к UI-фреймворку.