Интеграция liveQuery с промисами и асинхронным кодом

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

liveQuery возвращает Observable-подобный поток, который пересчитывает результат при каждом изменении затронутых таблиц. При этом каждая итерация вычисления внутри liveQuery может содержать промисы, что создаёт связку:

  • реактивный слой (подписка и пересчёт)
  • асинхронный слой (Promise/async-await внутри запроса)

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


Модель выполнения liveQuery с асинхронными зависимостями

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

const stream$ = Dexie.liveQuery(async () => {
    const users = await db.users.toArray();
    return users;
});

Поведение:

  • вызов toArray() возвращает Promise
  • liveQuery ждёт его завершения
  • результат эмитится в поток
  • любое изменение таблицы users инициирует повторный запуск функции

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


Комбинирование нескольких асинхронных источников

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

const dashboard$ = Dexie.liveQuery(async () => {
    const [users, orders, settings] = await Promise.all([
        db.users.toArray(),
        db.orders.toArray(),
        db.settings.get("main")
    ]);

    return {
        usersCount: users.length,
        ordersCount: orders.length,
        theme: settings?.theme
    };
});

Особенность модели:

  • Promise.all позволяет параллелизовать запросы
  • любое изменение в users, orders или settings вызывает повторный запуск
  • результат всегда консистентен на момент завершения всех промисов

Интеграция liveQuery с внешними Promise API

Асинхронные API вне Dexie могут быть включены в реактивный поток, но требуют аккуратного контроля побочных эффектов.

async function fetchExchangeRate() {
    const res = await fetch("/api/rate");
    return res.json();
}

const rate$ = Dexie.liveQuery(async () => {
    const [rate, items] = await Promise.all([
        fetchExchangeRate(),
        db.cart.toArray()
    ]);

    return items.map(i => ({
        ...i,
        priceInUsd: i.price * rate.value
    }));
});

С точки зрения исполнения:

  • внешний Promise становится частью реактивной цепочки
  • каждое изменение таблицы cart приводит к повторному fetch
  • отсутствует кэширование между пересчётами, если не реализовано отдельно

Управление конкуренцией и повторными пересчётами

Повторные изменения базы могут запускать несколько параллельных вычислений. Dexie отменяет устаревшие вычисления через внутреннюю систему подписок, но внешние Promise (например fetch) могут продолжить выполнение.

Типичная проблема:

  • запрос A запущен
  • база изменилась
  • запрос B запущен
  • A завершился позже B

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

Решение — локальная дедупликация или использование AbortController:

let controller;

const data$ = Dexie.liveQuery(async () => {
    controller?.abort();
    controller = new AbortController();

    const res = await fetch("/api/data", {
        signal: controller.signal
    });

    return res.json();
});

Преобразование liveQuery в Promise

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

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

import { firstValueFrom } from "rxjs";

const snapshot = await firstValueFrom(
    Dexie.liveQuery(() => db.users.toArray())
);

Поведение:

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

Это превращает реактивный источник в одноразовый snapshot.


Встраивание async/await внутри liveQuery

Асинхронная функция внутри liveQuery допускает полный async/await синтаксис, включая последовательные шаги.

const report$ = Dexie.liveQuery(async () => {
    const users = await db.users.toArray();

    const enriched = [];

    for (const user of users) {
        const profile = await db.profiles.get(user.id);
        enriched.push({ ...user, profile });
    }

    return enriched;
});

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

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

Использование liveQuery внутри транзакций

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

const stats$ = Dexie.liveQuery(async () => {
    return db.transaction("r", db.orders, async () => {
        const orders = await db.orders.toArray();

        return {
            total: orders.length,
            sum: orders.reduce((a, b) => a + b.amount, 0)
        };
    });
});

Семантика:

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

Ошибки внутри async liveQuery и их распространение

Любая ошибка в Promise внутри liveQuery становится ошибкой потока.

const broken$ = Dexie.liveQuery(async () => {
    const data = await db.nonExistingTable.toArray(); // ошибка
    return data;
});

Поведение:

  • ошибка эмитится как error event
  • поток может быть завершён или перезапущен (в зависимости от подписчика)
  • повторные изменения базы могут инициировать повторный запуск

Обработка:

const safe$ = Dexie.liveQuery(async () => {
    try {
        return await db.users.toArray();
    } catch (e) {
        return [];
    }
});

Связывание нескольких liveQuery потоков

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

const users$ = Dexie.liveQuery(() => db.users.toArray());
const posts$ = Dexie.liveQuery(() => db.posts.toArray());

const combined$ = Dexie.liveQuery(async () => {
    const [users, posts] = await Promise.all([
        users$.toPromise(),
        posts$.toPromise()
    ]);

    return {
        users,
        posts
    };
});

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


Частые архитектурные ошибки при смешивании liveQuery и Promise

Неправильное управление асинхронностью приводит к деградации реактивной модели:

  • неявное кэширование отсутствует → повторные API вызовы при каждом изменении
  • долгие await-цепочки → задержка эмиссий
  • внешние Promise без отмены → утечки ресурсов
  • смешивание синхронных и асинхронных источников без контроля порядка → гонки данных

Корректная модель требует:

  • минимизации внешних side effects внутри liveQuery
  • явного контроля отмены операций
  • разделения вычислений и I/O

Модель “reactive snapshot + async enrichment”

Наиболее устойчивый паттерн сочетания Promises и liveQuery заключается в разделении этапов:

  1. liveQuery формирует стабильный snapshot из IndexedDB
  2. внешний async слой обогащает данные
const base$ = Dexie.liveQuery(() => db.users.toArray());

const enriched$ = Dexie.liveQuery(async () => {
    const users = await base$.toPromise();

    return Promise.all(
        users.map(async user => {
            const stats = await fetch(`/api/stats/${user.id}`).then(r => r.json());
            return { ...user, stats };
        })
    );
});

Здесь реактивность ограничена базовым слоем, а асинхронное обогащение отделено логически.