Тема
Кэш, разделы и sharing
Remote работает с логическими query keys. QueryRuntime превращает их в физические keys, добавляя data partition и owner.
Логический ключ remote
ts
["billing", "invoices"]Remote видит только его. Runtime концептуально добавляет:
text
applicationId
tenantId
userId
private instanceId или shared marker
logical queryKeyТочный служебный формат является внутренней деталью. Не стройте физический key вручную.
Data partition
Одна partition определяется host application и session identity:
ts
const scope = runtime.createParticipantScope({
participantId: "billing-remote",
participantType: "remote",
tenantId: "tenant-a",
userId: "user-42",
capabilities: {
queryNamespaces: [["billing"]],
mutationNamespaces: ["billing"],
},
})Даже shared query не пересекается между tenant-a/user-42 и tenant-b/user-77.
Private cache по умолчанию
Создадим два mount одного remote:
ts
const firstScope = runtime.createParticipantScope(registration)
const secondScope = runtime.createParticipantScope(registration)У них одинаковый participantId, но разные instanceId. Если оба создадут:
ts
queryKey: () => ["billing", "draft"]получатся две private cache entries. Изменение draft первого mount не затронет второй.
Private cache подходит для:
- данных конкретного экрана;
- черновика одного mount;
- query, definition которой принадлежит только remote;
- данных, которые не должны разделяться случайно.
Shared cache
Host явно разрешает sharing:
ts
capabilities: {
queryNamespaces: [
["billing"],
["shared", "currency"],
],
sharedQueryNamespaces: [
["shared", "currency"],
],
mutationNamespaces: ["billing"],
}Query под shared prefix должна иметь definitionId:
ts
const currencies = scope.query.createStore<Currency[]>({
definitionId: "currency.dictionary.v1",
queryKey: () => ["shared", "currency", "list"],
queryFn: ({ signal }) => api.listCurrencies({ signal }),
staleTime: 60 * 60_000,
})Если billing и support scopes одной partition создадут одинаковый logical key с одинаковым definition, они увидят одну cache entry и один in-flight request.
Зачем нужен definitionId
Один query key должен иметь одно определение:
- одинаковый смысл ответа;
- совместимую schema;
- эквивалентную API-функцию;
- согласованную cache policy.
definitionId: "currency.dictionary.v1" — стабильное имя этого договора. Scoped runtime требует непустой id для shared query. Если response contract несовместимо изменился, используйте новую версию:
ts
definitionId: "currency.dictionary.v2"
queryKey: () => ["shared", "currency", "list", "v2"]Не используйте случайный definitionId: он должен оставаться стабильным между consumers одного shared contract.
Runtime проверяет наличие definitionId при каждом вычислении динамического shared key. QueryStore, InfiniteQueryStore и FetchStore передают id в cache; разные id одного физического ключа приводят к QueryDefinitionConflictError.
Shared FetchStore
У scoped FetchStore definition находится внутри options:
ts
const currency = scope.fetch.createStore<Currency, [string]>({
queryKey: (code) => ["shared", "currency", "details", code],
request: (code, config) => api.currency(code, config),
options: {
definitionId: "currency.details.v1",
staleTime: 60 * 60_000,
},
})Invalidation shared query
Любой participant с доступом к shared namespace может инвалидировать разрешённый key:
ts
await scope.query.invalidate(
{
queryKey: ["shared", "currency", "list"],
exact: true,
},
{
refetchActive: true,
},
)Активные observers других participants получат обновлённое состояние.
Что не стоит разделять
Не включайте в shared namespace без отдельной threat model:
- access/refresh token;
- permission matrix;
- платёжные реквизиты;
- персональные черновики;
- данные, definition которых различается у remote;
- временное UI-состояние.
Shared не означает «публичный». Partition всё равно разделяет tenant/user, но все разрешённые participants этой partition увидят данные.
Смена пользователя
Не меняйте userId у уже созданного scope. Identity immutable. На logout:
- unmount remote UI;
- dispose remote scopes;
- dispose persistence/sync controllers;
- dispose old runtime;
- после новой авторизации создайте новый runtime/scopes.
Так cache предыдущего пользователя не переедет в новую session.
Два remote: пример разрешений
ts
const billingScope = runtime.createParticipantScope({
participantId: "billing-remote",
participantType: "remote",
tenantId,
userId,
capabilities: {
queryNamespaces: [
["billing"],
["shared", "currency"],
],
sharedQueryNamespaces: [["shared", "currency"]],
mutationNamespaces: ["billing"],
},
})
const supportScope = runtime.createParticipantScope({
participantId: "support-remote",
participantType: "remote",
tenantId,
userId,
capabilities: {
queryNamespaces: [
["support"],
["shared", "currency"],
],
sharedQueryNamespaces: [["shared", "currency"]],
mutationNamespaces: ["support"],
},
})| Данные | Billing | Support | Shared |
|---|---|---|---|
["billing", "invoices"] | да | нет | нет |
["support", "tickets"] | нет | да | нет |
["shared", "currency", "list"] | да | да | да |
Persistence и sharing
Persistence подключается к root runtime host, а не к отдельному scope. В storage попадут только query, разрешённые policy и отмеченные meta.persist.
Storage key должен учитывать data partition. Подробнее: localStorage и persistence.