Тема
Host и два remote
В примере shell монтирует billing и support. У каждого remote свой private namespace, но currency dictionary общая.
1. Host runtime
ts
const runtime = createQueryRuntime({
mode: "host",
identity: {
applicationId: "dashboard-shell",
version: "4.0.0",
},
capabilities: ["request", "fetch", "query", "mutation"],
})
await runtime.initialize()2. Typed API adapters
ts
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)
},
currencies: (config) => {
return authApi.get("/api/v1/dictionaries/currencies", config)
},
}
const supportApi: SupportApi = {
listTickets: (config) => {
return authApi.get("/api/v1/support/tickets", config)
},
closeTicket: (ticketId, config) => {
return authApi.post(`/api/v1/support/tickets/${ticketId}/close`, undefined, config)
},
currencies: (config) => {
return authApi.get("/api/v1/dictionaries/currencies", config)
},
}Оба adapters могут использовать один host authApi, но remote не получают сам Axios instance.
3. Billing scope
ts
const billingScope = runtime.createParticipantScope({
participantId: "billing-remote",
participantType: "remote",
version: "2.1.0",
tenantId,
userId,
requiredRuntimeCapabilities: ["query", "mutation"],
capabilities: {
queryNamespaces: [["billing"], ["shared", "currency"]],
sharedQueryNamespaces: [["shared", "currency"]],
mutationNamespaces: ["billing"],
},
})4. Support scope
ts
const supportScope = runtime.createParticipantScope({
participantId: "support-remote",
participantType: "remote",
version: "1.8.0",
tenantId,
userId,
requiredRuntimeCapabilities: ["query", "mutation"],
capabilities: {
queryNamespaces: [["support"], ["shared", "currency"]],
sharedQueryNamespaces: [["shared", "currency"]],
mutationNamespaces: ["support"],
},
})5. Host mount
ts
const billingRemote = await import("billing/bootstrap")
const supportRemote = await import("support/bootstrap")
const unmountBillingUi = billingRemote.mount({
container: document.getElementById("billing-root")!,
api: billingApi,
scope: billingScope,
})
const unmountSupportUi = supportRemote.mount({
container: document.getElementById("support-root")!,
api: supportApi,
scope: supportScope,
})Вот единственный механизм передачи: host вызывает mount и кладёт scope в options.
6. Billing domain store
ts
export class BillingStore {
private readonly invoicesHandler
private readonly payHandler
private readonly currencyHandler
constructor(scope: IScopedQueryRuntime, api: BillingApi) {
this.invoicesHandler = scope.query.createStore({
queryKey: () => ["billing", "invoices"],
queryFn: ({ signal }) => api.listInvoices({ signal }),
})
this.payHandler = scope.mutation.createStore({
mutationKey: "billing.pay",
request: (invoiceId: string, config) => {
return api.payInvoice(invoiceId, config)
},
options: {
// Invalidation выполняем через scope facade с logical key.
onSuccess: () => {
void scope.query.invalidate({ queryKey: ["billing", "invoices"] }, { refetchActive: true })
},
},
})
this.currencyHandler = scope.query.createStore({
// Одинаковый definition используется в обоих remote.
definitionId: "currency.dictionary.v1",
queryKey: () => ["shared", "currency", "list"],
queryFn: ({ signal }) => api.currencies({ signal }),
staleTime: 60 * 60_000,
})
makeAutoObservable<this, "invoicesHandler" | "payHandler" | "currencyHandler">(
this,
{
invoicesHandler: false,
payHandler: false,
currencyHandler: false,
},
{ autoBind: true },
)
}
get invoices(): Invoice[] {
return this.invoicesHandler.data ?? []
}
pay(invoiceId: string): Promise<unknown> {
return this.payHandler.execute(invoiceId)
}
dispose(): void {
this.invoicesHandler.dispose()
this.payHandler.dispose()
this.currencyHandler.dispose()
}
}7. Support shared dictionary
ts
const currencyHandler = supportScope.query.createStore({
definitionId: "currency.dictionary.v1",
queryKey: () => ["shared", "currency", "list"],
queryFn: ({ signal }) => supportApi.currencies({ signal }),
staleTime: 60 * 60_000,
})Так как tenant/user partition и logical key совпадают, billing и support используют одну shared cache entry. Одинаковый definitionId фиксирует общий contract на уровне конфигурации и участвует в cache-level conflict check. Одновременный первый запрос deduplicate-ится.
Billing invoice query и support ticket query остаются private и недоступны другому remote.
8. Проверка запрета
ts
// Support scope не имеет billing namespace.
expect(() => {
supportScope.query.getData(["billing", "invoices"])
}).toThrow()Ошибка возникает до чтения cache и до HTTP.
9. Unmount
ts
// Сначала удаляем React UI.
unmountBillingUi()
unmountSupportUi()
// Затем освобождаем participant resources.
billingScope.dispose()
supportScope.dispose()
// Root runtime живёт дальше, пока работает shell.При полном shutdown:
ts
await runtime.dispose()Для route-by-route варианта каждый route хранит disposer только своего remote.