Dexie.js предоставляет два фундаментальных подхода к работе с
данными: классические промисы и реактивные потоки через
liveQuery. Их совместное использование формирует гибридную
модель, в которой императивные асинхронные операции дополняются
непрерывным наблюдением за изменениями состояния базы данных.
liveQuery возвращает Observable-подобный поток, который
пересчитывает результат при каждом изменении затронутых таблиц. При этом
каждая итерация вычисления внутри liveQuery может содержать
промисы, что создаёт связку:
Ключевая особенность заключается в том, что liveQuery
ожидает разрешения всех промисов внутри функции перед эмиссией
значения.
Функция, передаваемая в liveQuery, может быть
async, что автоматически приводит к возврату Promise. Dexie
интерпретирует результат как поток значений, где каждое изменение базы
запускает новый асинхронный цикл вычисления.
const stream$ = Dexie.liveQuery(async () => {
const users = await db.users.toArray();
return users;
});
Поведение:
toArray() возвращает PromiseliveQuery ждёт его завершения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 вызывает повторный запускАсинхронные 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
}));
});
С точки зрения исполнения:
cart приводит к повторному
fetchПовторные изменения базы могут запускать несколько параллельных
вычислений. Dexie отменяет устаревшие вычисления через внутреннюю
систему подписок, но внешние Promise (например fetch) могут
продолжить выполнение.
Типичная проблема:
Результат: возможна гонка значений, если не учитывать порядок эмиссий.
Решение — локальная дедупликация или использование 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();
});
Несмотря на реактивную природу, часто требуется получить одно значение, соответствующее текущему состоянию базы.
Подход заключается в извлечении первого эмитированного значения.
import { firstValueFrom } from "rxjs";
const snapshot = await firstValueFrom(
Dexie.liveQuery(() => db.users.toArray())
);
Поведение:
liveQuery создаёт потокfirstValueFrom подписываетсяЭто превращает реактивный источник в одноразовый snapshot.
Асинхронная функция внутри 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;
});
Особенности:
Асинхронная логика внутри 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)
};
});
});
Семантика:
Любая ошибка в Promise внутри liveQuery становится
ошибкой потока.
const broken$ = Dexie.liveQuery(async () => {
const data = await db.nonExistingTable.toArray(); // ошибка
return data;
});
Поведение:
Обработка:
const safe$ = Dexie.liveQuery(async () => {
try {
return await db.users.toArray();
} catch (e) {
return [];
}
});
Асинхронная композиция позволяет объединять несколько реактивных источников.
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
};
});
Такой подход формирует каскад реактивных зависимостей, где один поток становится входом для другого.
Неправильное управление асинхронностью приводит к деградации реактивной модели:
Корректная модель требует:
Наиболее устойчивый паттерн сочетания Promises и liveQuery заключается в разделении этапов:
liveQuery формирует стабильный snapshot из
IndexedDBconst 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 };
})
);
});
Здесь реактивность ограничена базовым слоем, а асинхронное обогащение отделено логически.