Skip to content

Кэш, разделы и 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:

  1. unmount remote UI;
  2. dispose remote scopes;
  3. dispose persistence/sync controllers;
  4. dispose old runtime;
  5. после новой авторизации создайте новый 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"],
  },
})
ДанныеBillingSupportShared
["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.