Тема
Фасады IScopedQueryRuntime
Host передаёт remote объект типа IScopedQueryRuntime. Он не выполняет HTTP напрямую. Он создаёт MobX stores, подключённые к общему host runtime.
ts
interface IScopedQueryRuntime {
identity: IParticipantIdentity
operationContext: IRequestOperationContext
request: IScopedRequestApi
fetch: IScopedFetchApi
query: IScopedQueryApi
mutation: IScopedMutationApi
dispose(): void
}scope.request
Методы
| Метод | Возвращает | Для чего |
|---|---|---|
createStore(config) | RequestStore | Прямой запрос без query cache. |
ts
const exportReport = scope.request.createStore({
// API-функция передана remote через typed adapter.
api: (reportId: string, config) => api.exportReport(reportId, config),
options: {
retry: false,
},
})
const response = await exportReport.execute("report-42")Runtime автоматически передаёт общий request executor и owner context. Remote не видит эти внутренние dependencies.
Полная семантика: RequestStore.
scope.fetch
Методы
| Метод | Возвращает | Для чего |
|---|---|---|
createStore(config) | FetchStore | Ручное кэшируемое чтение. |
ts
const invoiceDetails = scope.fetch.createStore<Invoice, [string]>({
// Logical key сначала проверяется namespace policy.
queryKey: (invoiceId) => ["billing", "invoice", invoiceId],
// API-функция вызывается только после явного fetch.
request: (invoiceId, config) => api.invoice(invoiceId, config),
options: {
staleTime: 60_000,
},
})
await invoiceDetails.fetch("invoice-42")Scope подменяет client на общий QueryClient runtime и преобразует logical key в физический partitioned key.
Полная семантика: FetchStore.
scope.query
Методы
| Метод | Возвращает | Для чего |
|---|---|---|
createStore(config) | QueryStore | Автоматическое кэшируемое чтение. |
createInfiniteStore(config) | InfiniteQueryStore | Cursor pagination. |
getData(queryKey) | TData | undefined | Синхронно прочитать разрешённую cache entry. |
invalidate(filters, options?) | Promise<void> | void | Инвалидировать разрешённый query. |
createStore
ts
const invoices = scope.query.createStore<Invoice[]>({
queryKey: () => ["billing", "invoices"],
queryFn: ({ signal }) => api.listInvoices({ signal }),
staleTime: 30_000,
})Factory сразу запускает lifecycle, если autoStart !== false.
createInfiniteStore
ts
const activity = scope.query.createInfiniteStore<ActivityPage>({
queryKey: () => ["billing", "activity"],
initialPageParam: null,
queryFn: ({ pageParam, signal }) => {
const cursor = typeof pageParam === "string" ? pageParam : null
return api.activity(cursor, { signal })
},
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
})getData
ts
const cachedInvoices = scope.query.getData<Invoice[]>(["billing", "invoices"])Метод не запускает HTTP и не создаёт подписку. Он возвращает текущий snapshot или undefined. UI обычно читает QueryStore.data, а getData используют в composition/domain logic.
invalidate
ts
await scope.query.invalidate(
{
queryKey: ["billing", "invoices"],
exact: true,
},
{
// Активный QueryStore сразу выполнит refetch.
refetchActive: true,
},
)Без globalInvalidation: true remote обязан передать конкретный queryKey. Пользовательский predicate запрещён, чтобы remote не анализировал структуру чужих cache entries.
Полные API: QueryStore и InfiniteQueryStore.
scope.mutation
Методы
| Метод | Возвращает | Для чего |
|---|---|---|
createStore(config) | MutationStore | Изменяющая операция с lifecycle callbacks. |
ts
const payInvoice = scope.mutation.createStore<Invoice, [string]>({
// Проверяется по mutationNamespaces participant.
mutationKey: "billing.pay",
request: (invoiceId, config) => api.payInvoice(invoiceId, config),
options: {
retry: false,
// Используем facade scope, чтобы logical key получил partition prefix.
onSuccess: () => {
void scope.query.invalidate({ queryKey: ["billing", "invoices"] }, { refetchActive: true })
},
},
})
await payInvoice.execute("invoice-42")Полная семантика: MutationStore.
scope.identity
Read-only описание текущего mount:
ts
scope.identity.participantId
scope.identity.instanceId
scope.identity.participantType
scope.identity.version
scope.identity.tenantId
scope.identity.userId
scope.identity.protocolVersionНе используйте identity как replacement auth-store. Она нужна для ownership, partition и diagnostics.
scope.operationContext
Read-only базовый контекст операций participant:
ts
scope.operationContext.applicationId
scope.operationContext.ownerId
scope.operationContext.participantId
scope.operationContext.tenantId
scope.operationContext.userIdRuntime добавляет correlation id отдельным request/mutation execution. Контекст не содержит payload и credential.
scope.dispose()
ts
scope.dispose()Метод:
- вызывает
dispose()всех stores, созданных этим scope; - отменяет direct requests, принадлежащие participant;
- удаляет participant из runtime;
- запрещает новые операции через этот scope.
Повторный вызов безопасен.
Domain store remote с двумя handlers
UI не должен получать scope. Remote composition создаёт предметный store:
ts
export class BillingStore {
private readonly listHandler
private readonly payHandler
constructor(scope: IScopedQueryRuntime, api: BillingApi) {
this.listHandler = scope.query.createStore({
queryKey: () => ["billing", "invoices"],
queryFn: ({ signal }) => api.listInvoices({ signal }),
})
this.payHandler = scope.mutation.createStore({
mutationKey: "billing.pay",
request: (id: string, config) => api.payInvoice(id, config),
options: {
onSuccess: () => {
void scope.query.invalidate({ queryKey: ["billing", "invoices"] }, { refetchActive: true })
},
},
})
makeAutoObservable<this, "listHandler" | "payHandler">(
this,
{
listHandler: false,
payHandler: false,
},
{
autoBind: true,
},
)
}
get invoices(): Invoice[] {
return this.listHandler.data ?? []
}
get loading(): boolean {
return this.listHandler.loading || this.payHandler.loading
}
pay(invoiceId: string): Promise<AxiosResponse<Invoice> | null> {
return this.payHandler.execute(invoiceId)
}
dispose(): void {
this.listHandler.dispose()
this.payHandler.dispose()
}
}scope.dispose() остаётся страховочной границей host, даже если remote сам освободил domain stores.