Skip to content

Фасады 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)InfiniteQueryStoreCursor 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.userId

Runtime добавляет 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.