Тема
Как remote получает scope
Remote получает scope как обычный аргумент функции mount. Host загружает JavaScript module remote в свою страницу, поэтому может вызвать экспортированную функцию и передать ей объект.
Ни router, ни federation plugin, ни Query Layer не делают скрытой инъекции.
1. Общий TypeScript contract
ts
import type { AxiosRequestConfig, AxiosResponse } from "axios"
import type { IScopedQueryRuntime } from "@dubium/query-layer/mf"
export interface Invoice {
id: string
amount: number
status: "open" | "paid"
}
// Remote видит только разрешённые billing endpoints.
// Здесь нет универсального request(method, url).
export interface BillingApi {
listInvoices(config?: AxiosRequestConfig): Promise<AxiosResponse<Invoice[]>>
payInvoice(invoiceId: string, config?: AxiosRequestConfig): Promise<AxiosResponse<Invoice>>
}
// Всё, что host передаёт одному mount remote.
export interface BillingBootstrapOptions {
container: HTMLElement
api: BillingApi
scope: IScopedQueryRuntime
}
export interface BillingRemoteModule {
mount(options: BillingBootstrapOptions): () => void
}Contract можно публиковать отдельным types-only пакетом. В него не нужно помещать token, Axios instance host или root QueryRuntime.
2. Host реализует API adapter
ts
import type { BillingApi } from "@portal/contracts"
import { authApi } from "../data/api"
// Host сохраняет контроль над Authorization и refresh flow.
export const billingApi: BillingApi = {
listInvoices: (config) => {
return authApi.get("/api/v1/billing/invoices", config)
},
payInvoice: (invoiceId, config) => {
return authApi.post(`/api/v1/billing/invoices/${invoiceId}/pay`, undefined, config)
},
}Remote получает функции конкретной предметной области, но не может отправить произвольный запрос на любой URL.
3. Host создаёт scope для mount
ts
import type { QueryRuntime } from "@dubium/query-layer/runtime"
import type { BillingRemoteModule } from "@portal/contracts"
import { billingApi } from "./billing-api"
interface MountBillingRemoteOptions {
container: HTMLElement
runtime: QueryRuntime
tenantId: string
userId: string
}
export const mountBillingRemote = async (options: MountBillingRemoteOptions): Promise<() => void> => {
// Host загружает remote module через federation alias.
const remote: BillingRemoteModule = await import("billing/bootstrap")
// Scope создаётся для этого конкретного mount.
const scope = options.runtime.createParticipantScope({
participantId: "billing-remote",
participantType: "remote",
version: "1.6.0",
// Remote требует совместимый host до mount UI.
minimumRuntimeVersion: "3.2.0",
requiredRuntimeCapabilities: ["query", "mutation"],
// Identity берётся из доверенной host session.
tenantId: options.tenantId,
userId: options.userId,
// Remote может работать только с billing keys.
capabilities: {
queryNamespaces: [["billing"]],
mutationNamespaces: ["billing"],
globalInvalidation: false,
},
})
try {
// Вот точное место, где remote получает scope.
const unmountUi = remote.mount({
container: options.container,
api: billingApi,
scope,
})
return () => {
// UI перестаёт читать stores.
unmountUi()
// Затем host освобождает все stores и requests participant.
scope.dispose()
}
} catch (error) {
// Если mount упал, scope всё равно нельзя оставлять живым.
scope.dispose()
throw error
}
}4. Router вызывает composition function
Host может полностью отвечать за routing. Это не конфликтует с передачей scope:
ts
import { queryRuntime } from "../app/query-runtime"
import { sessionStore } from "../auth/session.store"
import { mountBillingRemote } from "../remotes/mount-billing-remote"
export const enterBillingRoute = async (container: HTMLElement): Promise<() => void> => {
// Router передал container route.
// Composition layer добавила runtime и identity host.
return mountBillingRemote({
container,
runtime: queryRuntime,
tenantId: sessionStore.tenantId,
userId: sessionStore.userId,
})
}Router может хранить returned disposer и вызвать его при уходе с route.
5. Remote принимает options
ts
import { createRoot } from "react-dom/client"
import type { BillingBootstrapOptions } from "@portal/contracts"
import { BillingApp } from "./BillingApp"
import { BillingStore } from "./data/billing.store"
export const mount = (options: BillingBootstrapOptions): (() => void) => {
// Domain store получает scope и узкий API contract.
const store = new BillingStore(options.scope, options.api)
const root = createRoot(options.container)
// React получает уже предметный store.
root.render(<BillingApp store={store} />)
return () => {
root.unmount()
store.dispose()
}
}6. Store remote создаёт handler
ts
import { makeAutoObservable } from "mobx"
import type { QueryStore } from "@dubium/query-layer"
import type { IScopedQueryRuntime } from "@dubium/query-layer/mf"
import type { BillingApi, Invoice } from "@portal/contracts"
export class BillingStore {
private readonly invoicesHandler: QueryStore<Invoice[]>
constructor(scope: IScopedQueryRuntime, api: BillingApi) {
this.invoicesHandler = scope.query.createStore({
// Ключ разрешён queryNamespaces [["billing"]].
queryKey: () => ["billing", "invoices"],
// HTTP выполняется через API adapter, переданный host.
queryFn: ({ signal }) => api.listInvoices({ signal }),
staleTime: 30_000,
})
makeAutoObservable<this, "invoicesHandler">(this, { invoicesHandler: false }, { autoBind: true })
}
get invoices(): Invoice[] {
return this.invoicesHandler.data ?? []
}
get loading(): boolean {
return this.invoicesHandler.loading
}
dispose(): void {
this.invoicesHandler.dispose()
}
}Попытка создать key ['support', 'tickets'] завершится ошибкой доступа до HTTP-запроса.
Почему передаются и scope, и api
Они решают разные задачи:
| Объект | Ответственность |
|---|---|
api | Конкретные HTTP endpoints и auth transport host. |
scope | MobX store factories, общий cache, ownership и namespace policy. |
scope не содержит Axios client и не имеет универсального метода get(url). api не хранит MobX-состояние.
Standalone development remote
Для запуска remote без host создайте local adapter и local runtime, но используйте тот же bootstrap contract:
ts
import { createQueryRuntime } from "@dubium/query-layer/runtime"
const localRuntime = createQueryRuntime({
mode: "standalone-remote",
identity: {
applicationId: "billing-local",
version: "1.6.0",
},
})
await localRuntime.initialize()
const localScope = localRuntime.createParticipantScope({
participantId: "billing-local-mount",
participantType: "application",
})
const unmount = mount({
container: document.getElementById("root")!,
api: localBillingApi,
scope: localScope,
})Domain store remote не меняется между standalone и host режимами.
Дальше: основные понятия participant scope.