Тема
Persistence
Persistence сохраняет выбранные успешные query из QueryRuntime, чтобы после reload восстановить cache до нового HTTP-запроса.
Mutation state и direct RequestStore не сохраняются.
Простой пример с localStorage
ts
import { createSyncStoragePersister, persistQueryRuntime } from "@dubium/query-layer/persist"
import { runtime } from "./query-runtime"
// Persister знает только способ read/write/remove snapshot.
export const persister = createSyncStoragePersister({
key: "portal:public-query-cache",
storage: window.localStorage,
onError: (error) => {
console.error("Persistence storage error", error)
},
})
// Controller связывает persister с cache конкретного runtime.
export const persistence = await persistQueryRuntime({
runtime,
persister,
// Snapshot другой версии deployment будет удалён.
buster: "portal:api-v1",
// Snapshot старше часа не восстанавливаем.
maxAge: 60 * 60_000,
// Объединяем частые cache events в один write.
throttleTime: 1_000,
// По умолчанию true; указываем явно как security decision.
rejectSensitiveData: true,
})Query должна дать opt-in
ts
// Обычный application scope создаётся один раз после startup runtime.
const appScope = runtime.createParticipantScope({
participantId: "portal-app",
participantType: "application",
})
const countries = appScope.query.createStore<Country[]>({
queryKey: () => ["dictionary", "countries"],
queryFn: ({ signal }) => dictionaryApi.countries({ signal }),
staleTime: 24 * 60 * 60_000,
meta: {
// Default persistence policy сохраняет только такие query.
persist: true,
},
})Без meta.persist === true default policy пропускает query.
Что происходит при startup
ts
const runtime = createQueryRuntime(...)
// restoreClient → validation → migration → hydrate.
const persistence = await persistQueryRuntime({ runtime, persister })
await runtime.initialize()
// Store сразу увидит hydrated cache, если key совпадает.
const appScope = runtime.createParticipantScope(...)Если snapshot повреждён, слишком стар, имеет другой buster или не может быть мигрирован, он удаляется.
Snapshot хранит вид каждой записи cache. Поэтому InfiniteQueryStore после cold start восстанавливает { pages, pageParams } как InfiniteQuery и связывает реальную queryFn при создании store, не конфликтуя с обычным Query того же key.
Поддерживаемые значения
Стандартный serializer сохраняет plain objects (включая объекты с null prototype), arrays, undefined, finite и специальные числовые значения, BigInt, Date, RegExp, Map и Set. Циклические ссылки, functions, symbols и экземпляры пользовательских классов отклоняются ошибкой. Это предотвращает незаметную потерю prototype и методов после restore. Для иной domain-модели передайте собственные serialize/deserialize и восстановите тип явно.
localStorage или sessionStorage
ts
const localPersister = createSyncStoragePersister({
key: "portal-cache",
storage: window.localStorage,
})
const tabPersister = createSyncStoragePersister({
key: "portal-tab-cache",
storage: window.sessionStorage,
})| Storage | Жизнь | Подходит |
|---|---|---|
localStorage | Между reload и перезапуском браузера | Маленький разрешённый cache |
sessionStorage | Пока открыта вкладка | Временный cache одной вкладки |
Оба API синхронные и доступны JavaScript того же origin. Не храните secrets.
IndexedDB
ts
import { createIndexedDbPersister, persistQueryRuntime } from "@dubium/query-layer/persist"
const persister = createIndexedDbPersister({
databaseName: "portal-query-cache",
databaseVersion: 1,
storeName: "runtime",
key: "public-dictionaries",
onError: reportPersistenceError,
})
const persistence = await persistQueryRuntime({
runtime,
persister,
})IndexedDB работает асинхронно и лучше подходит для более крупных разрешённых payload.
Все options persistQueryRuntime
| Option | Type | Default | Что делает |
|---|---|---|---|
runtime | IQueryRuntimeContract | — | Runtime, cache которого сохраняется. Обязательно. |
persister | IPersister | — | Adapter read/write/remove. Обязательно. |
buster | string | — | Инвалидирует snapshot при несовместимой сборке/API. |
version | number | 2 | Версия persisted schema приложения. |
migrations | Record<number, TPersistMigration> | — | Последовательные миграции к новой version. |
maxAge | number | без лимита | Максимальный возраст snapshot в миллисекундах. |
throttleTime | number | 1000 | Задержка объединения writes. |
maxPayloadBytes | number | без project limit | Максимальный размер сериализованного snapshot. |
shouldPersistQuery | (query) => boolean | meta.persist === true | Дополнительная allowlist policy query. |
rejectSensitiveData | boolean | true | Запрещает snapshot с чувствительными полями. |
sensitiveDataCheck | ISensitiveDataCheckOptions | default guard | Настраивает forbidden keys/проверку. |
onPersistError | (error) => void | — | Получает restore/serialize/storage ошибки. |
clock | IClock | system clock | Внедряемое время для тестов. |
timeoutManager | ITimeoutManager | system timers | Внедряемый throttle scheduler. |
Options sync storage persister
| Option | Type | Default | Что делает |
|---|---|---|---|
key | string | QUERY_LAYER_CACHE | Ключ Web Storage. |
storage | Storage | window.localStorage | localStorage/sessionStorage/custom adapter. |
serialize | (client) => string | safe serializer | Преобразует snapshot в строку. |
deserialize | (value) => unknown | safe deserializer | Разбирает строку; результат ещё валидируется. |
onError | (error) => void | — | Получает Web Storage/codec ошибки. |
Options IndexedDB persister
| Option | Type | Default |
|---|---|---|
databaseName | string | dubium-query-layer |
databaseVersion | number | 1 |
storeName | string | query-cache |
key | IDBValidKey | persisted-client |
indexedDB | IDBFactory | globalThis.indexedDB |
onError | (error) => void | — |
Custom filter
ts
const persistence = await persistQueryRuntime({
runtime,
persister,
shouldPersistQuery: (query) => {
return query.meta?.persist === true && query.queryKey[0] === "dictionary"
},
})Callback получает копии queryKey, queryHash и meta, но не получает response body отдельным аргументом или методы Query.
Migration
ts
const persistence = await persistQueryRuntime({
runtime,
persister,
version: 3,
migrations: {
3: (clientV2) => migrateV2ToV3(clientV2),
},
})Переход выполняется последовательно. Если нужного шага нет или migration возвращает false, snapshot удаляется.
migrations получают только структурно валидные persisted clients. Исторические snapshots schema v1, созданные до обязательного поля kind, встроенные persisters отклоняют на этапе validation и удаляют: безопасно определить вид Query по форме пользовательских данных невозможно. Поэтому такие v1 snapshots не передаются в migrations[2] и инвалидируются без эвристик.
Controller lifecycle
| Метод | Что делает |
|---|---|
flush() | Немедленно пытается сохранить разрешённый cache. |
dispose() | Снимает subscription, выполняет финальный безопасный flush и останавливает controller. |
ts
await persistence.flush()
await persistence.dispose()
// При logout старой session удаляем snapshot.
await persister.removeClient()
// Только IndexedDB persister имеет собственное соединение.
await indexedDbPersister.dispose()User и tenant
Не используйте один storage key для разных пользователей. Включайте безопасный partition hash в key и удаляйте snapshot на logout. Для MF-порядка смотрите localStorage и persistence.