Тема
localStorage и persistence в MF
QueryRuntime сам ничего не записывает в storage. Host отдельно подключает persistence plugin к root runtime.
Remote только отмечает разрешённые query через metadata:
ts
meta: {
persist: true,
}Кто владеет persistence
| Действие | Host | Remote |
|---|---|---|
| Создать persister | да | нет |
| Выбрать storage key | да | нет |
| Определить tenant/user partition | да | нет |
| Включить opt-in у своей query | разрешает policy | ставит meta.persist |
| Flush/dispose при logout | да | нет |
Remote не должен создавать отдельный persister поверх того же runtime: появятся конкурирующие restore/flush lifecycles.
Подключение localStorage в host
ts
import {
createSyncStoragePersister,
persistQueryRuntime,
} from "@dubium/query-layer/persist"
import { queryRuntime } from "./query-runtime"
import { sessionStore } from "../auth/session.store"
import { createDataPartitionHash } from "../security/partition-hash"
// Не кладём raw userId/email в storage key.
// Эта проектная функция возвращает стабильный неперсональный hash partition.
const partitionHash = createDataPartitionHash({
tenantId: sessionStore.tenantId,
userId: sessionStore.userId,
})
export const queryPersister = createSyncStoragePersister({
// У каждой session partition отдельная запись.
key: `portal-query-cache:${partitionHash}`,
// Явно выбираем localStorage.
storage: window.localStorage,
onError: (error) => {
console.error("Не удалось сохранить query cache", error)
},
})
export const persistence = await persistQueryRuntime({
runtime: queryRuntime,
persister: queryPersister,
// При несовместимом deployment старый snapshot удалится.
buster: "portal:api-v3",
// Формат persisted data приложения.
version: 2,
// Старше 24 часов cache не восстанавливается.
maxAge: 24 * 60 * 60_000,
// Частые cache updates объединяются в один write.
throttleTime: 1_000,
// Защита включена по умолчанию; оставляем намеренно явно.
rejectSensitiveData: true,
// Дополнительные запрещённые имена полей проекта.
sensitiveDataCheck: {
forbiddenKeys: [
"password",
"token",
"authorization",
"secret",
],
},
// Ограничиваем размер одной JSON-записи.
maxPayloadBytes: 1_500_000,
})createDataPartitionHash не входит в Query Layer. Это project security helper. Не используйте обычный непосоленный hash email как защиту персональных данных.
Порядок startup
Для восстановления cache до создания remote stores:
ts
// 1. Создали runtime без browser side effects.
const runtime = createQueryRuntime(...)
// 2. Подключили persistence: здесь происходит restore/hydrate.
const persistence = await persistQueryRuntime({
runtime,
persister,
})
// 3. Подключили focus/online lifecycle.
await runtime.initialize()
// 4. Только теперь создаём scopes и монтируем remote.
const scope = runtime.createParticipantScope(...)Opt-in query в remote
ts
const currencies = scope.query.createStore<Currency[]>({
definitionId: "currency.dictionary.v1",
queryKey: () => ["shared", "currency", "list"],
queryFn: ({ signal }) => api.currencies({ signal }),
meta: {
// Разрешает persistence policy рассмотреть эту query.
persist: true,
},
})meta.persist: true не гарантирует запись: host policy может дополнительно запретить key.
ts
const persistence = await persistQueryRuntime({
runtime,
persister,
shouldPersistQuery: (query) => {
return query.meta?.persist === true &&
query.queryKey.includes("currency")
},
})Callback получает read-only queryKey, queryHash и копию meta. Он не получает живой Query или response headers.
Что допустимо хранить в localStorage
Подходящие данные:
- небольшой публичный dictionary;
- feature flags без secrets;
- редко меняющийся каталог, разрешённый data policy;
- технические справочники малого размера.
Не храните:
- access/refresh token;
- password;
- Authorization header;
- банковские/медицинские данные;
- полный персональный profile без явного требования;
- большой список, который может превысить Web Storage quota.
localStorage синхронный и блокирует main thread на serialize/write. Для объёмных разрешённых данных используйте IndexedDB.
sessionStorage
ts
const persister = createSyncStoragePersister({
key: `portal-session-cache:${partitionHash}`,
storage: window.sessionStorage,
})sessionStorage ограничен одной вкладкой и очищается после её закрытия, но это не делает его безопасным для tokens: JavaScript того же origin всё ещё может прочитать данные.
IndexedDB
ts
import { createIndexedDbPersister } from "@dubium/query-layer/persist"
const persister = createIndexedDbPersister({
databaseName: "portal-query-cache",
storeName: "runtime",
key: partitionHash,
})IndexedDB выполняет асинхронный I/O и лучше подходит для крупных payload. Data classification и partition rules остаются теми же.
Logout
ts
// 1. Remote UI больше не читает stores.
unmountAllRemotes()
// 2. Scope освобождает participant resources.
billingScope.dispose()
supportScope.dispose()
// 3. Останавливаем subscription и завершаем текущий flush.
await persistence.dispose()
// 4. Удаляем snapshot старого пользователя.
await queryPersister.removeClient()
// 5. Освобождаем root runtime.
await queryRuntime.dispose()Для IndexedDB после remove также вызовите await persister.dispose().
Несколько вкладок
Persistence сохраняет cache на диск, но сам не уведомляет другие открытые вкладки. Для live-update подключите cross-tab sync. Порядок и конфликтующие политики описаны на странице Совместное использование плагинов.