feat: Добавить VitePress

This commit is contained in:
2026-07-25 17:56:16 +03:00
parent cfcff10c58
commit 6f6e4896af
80 changed files with 4663 additions and 665 deletions

View File

@@ -0,0 +1,293 @@
---
title: Runtime-граница business
description: Строгая изоляция business-фабрики от источников данных, state/query runtime, infra, browser API и внешних ошибок
---
# Runtime-граница business
## Главный инвариант
Business-модуль выполняет доменную композицию только над:
- собственными типами и детерминированной логикой;
- capabilities, переданными фабрике через `{Domain}Deps`.
Business не вызывает runtime-возможность, если она не была передана фабрике. Это относится не только к данным и `infra`, но и к hooks, stores, subscriptions, browser API и другим concrete runtime-механизмам.
Фабрика отвечает на вопрос «какой стабильный доменный API нужен приложению», а не «какими библиотеками и источниками он реализован».
## Что разрешено внутри business
Business может напрямую использовать:
- собственные domain types;
- собственные services, mappers, normalizers, validators и type guards;
- собственные domain errors;
- детерминированные вычисления без I/O, runtime-state и скрытого окружения;
- чистые библиотеки вроде schema validators, decimal/date utilities, если результат определяется только явными аргументами и типы библиотеки не становятся public contract;
- type-only контракты других business API для cross-domain dependencies.
## Что передаётся через deps
Через `{Domain}Deps` передавай любую runtime-capability:
| Capability | Примеры concrete implementation |
|---|---|
| Product source | REST SDK, GraphQL client, CMS, storage |
| Source/query hook | SWR, TanStack Query, Apollo hook |
| Domain state runtime | Zustand, Redux, MobX, RxJS store |
| Technical service | logger, telemetry, notifications, i18n engine |
| Platform API | `window`, navigation, clipboard, geolocation |
| Lifecycle event | unauthorized event, socket event, subscription |
| Environment | env/config provider, feature runtime configuration |
| Nondeterminism | clock, timer, random, ID generator |
| Cross-domain behavior | ограниченный API другой business-фабрики |
Не импортируй concrete implementation в business даже в том случае, если библиотека используется только в одном внутреннем файле.
## Запрещённые imports
Production-код `business/**` не импортирует напрямую ни runtime values, ни types из concrete runtimes:
- `infra`, `compositions`, `app`;
- SDK, generated operations и HTTP clients;
- storage implementation;
- React state/effect APIs;
- SWR, TanStack Query, Apollo и другие query runtimes;
- Zustand, Redux, MobX, RxJS stores;
- browser и framework runtime APIs;
- event bus implementation;
- env и process-specific configuration.
Запрет нельзя обойти через `import type`, alias, barrel, type cast или helper в `shared`. Type-only разрешены собственные contracts, детерминированный `shared`, чистые libraries и суженные public API других business-доменов.
## Доменный шлюз данных
Business API является единственной продуктовой границей для потребительского кода.
```text
page / layout / screen / widget
→ {Domain}Api
→ business scenario
→ {Domain}Deps
→ private dependency adapter
→ infra client / SDK / storage / external source
```
Внешний сервис остаётся физическим источником данных. Business является единственным источником доменной истины: он определяет модель, сценарий, нормализацию, fallback и ошибки.
Обычный consumer composition не создаёт параллельный продуктовый контракт поверх DTO или client. Единственная зона concrete product integration внутри `compositions``compositions/business/{domain}`.
## Business-owned deps
`{Domain}Deps` принадлежит business-модулю и описывает необходимые возможности доменным языком.
Требования:
- группируй методы по capability, а не по имени SDK/client;
- принимай доменные аргументы;
- принимай внешние результаты как `unknown`, если нужна runtime-проверка;
- описывай собственную минимальную форму dependency hook/result;
- описывай собственный state port, а не `StoreApi` конкретной библиотеки;
- возвращай cleanup из subscription capability;
- не передавай mapper, normalizer или domain error через deps;
- не используй generated DTO как доменную модель;
- не передавай целый client, если домену нужны два конкретных действия.
Плохо:
```ts
import type { StoreApi } from 'zustand'
import type { AdminApiClient } from '@vendor/admin-sdk'
export type AuthDeps = {
api: AdminApiClient
store: StoreApi<AuthState>
}
```
Хорошо:
```ts
import type { AuthState } from './auth-state.type'
import type { VerifyPhoneCodeData } from './verify-phone-code-data.type'
export type SourceHookResult = {
data: unknown
error: unknown
isLoading: boolean
refresh: () => Promise<void>
}
export type AuthDeps = {
phoneAuth: {
requestCode: (phone: string) => Promise<unknown>
resendCode: (challengeId: string) => Promise<unknown>
verifyCode: (data: VerifyPhoneCodeData) => Promise<unknown>
}
session: {
setToken: (token?: string | null) => void
useToken: () => string | null | undefined
}
sessionEvents: {
onInvalidated: (listener: () => void) => () => void
}
state: {
create: (initialState: AuthState) => {
get: () => AuthState
set: (state: AuthState) => void
useState: () => AuthState
}
}
}
```
## Dependency adapters
Adapter находится снаружи business и реализует конкретную часть `{Domain}Deps`.
```text
compositions/business/auth/
├── create-auth-business.ts
├── adapters/
│ ├── admin-auth-session.adapter.ts
│ ├── browser-auth-navigation.adapter.ts
│ ├── zustand-auth-state.adapter.ts
│ └── admin-auth-session-events.adapter.ts
└── index.ts
```
Правила adapter:
- импортирует type-only business contract;
- импортирует concrete infra/runtime implementation;
- преобразует доменные аргументы в transport arguments;
- возвращает raw/unknown runtime result, если business должен его проверить;
- пробрасывает source error без создания domain error;
- не реализует бизнес-правило;
- не выбирает доменный fallback;
- не экспортируется через public API integration module;
- тестируется на wiring, payload и соответствие dependency contract.
Каждая runtime-capability получает явный adapter. Не скрывай несколько разных integrations как inline-функции внутри builder.
## Dependency hooks
Если business должен предоставить hook, concrete hook передаётся через deps.
```text
SWR/TanStack hook
→ dependency adapter
→ business-owned source hook contract
→ business wrapper hook
→ domain result / domain error
```
Business wrapper может:
- вызвать переданный dependency hook;
- нормализовать `data` в доменную модель;
- заменить source error доменной ошибкой;
- вычислить domain state и selectors;
- вернуть собственный стабильный result type.
Dependency hook обязан быть non-throwing и non-Suspense: technical failure возвращается в `error: unknown`, а не выбрасывается во время вызова hook. Business wrapper обязан заменить этот `error` собственной domain error. Публичные callbacks dependency result, например `refresh`, также оборачиваются business и не могут выдать source error наружу.
Business wrapper не импортирует query library. В public contract не протекают `SWRConfiguration`, `UseQueryResult`, query keys, cache implementation или library-specific mutate API без отдельного domain contract.
## Domain state
Business владеет:
- моделью доменного state;
- допустимыми переходами;
- commands и selectors;
- реакцией на dependency results и events.
Business не владеет concrete state manager. Business выбирает initial domain state и передаёт его в `deps.state.create(initialState)`; adapter только создаёт concrete store с переданным значением.
Zustand/Redux/MobX adapter реализует business-owned state adapter factory и создаётся в `compositions/business/{domain}`. Business-фабрика получает adapter factory через `deps`, передаёт initial domain state и получает concrete port.
Локальный UI-state является другим случаем. Состояние раскрытия sidebar, выбранной вкладки или шага локального UI-flow может использовать concrete state manager непосредственно внутри владеющего composition module, если оно не подменяет доменное состояние и product data boundary.
## Доменные ошибки
Из любого публичного метода, command, query или hook business-модуля выходят только собственные доменные ошибки.
Business обязан преобразовать:
- rejected promise dependency;
- synchronous throw dependency;
- source hook error;
- ошибку store/storage/browser API;
- невалидный успешный ответ;
- неизвестную runtime-ошибку.
Domain error содержит стабильный `code`. Исходная ошибка может сохраняться только в `cause` для диагностики.
Потребитель не ориентируется на:
- `message` внешней ошибки;
- HTTP status;
- `response`;
- SDK error class;
- stack или transport code.
Adapter не импортирует и не создаёт domain error. Error mapping всегда выполняет business.
Rejected promise, synchronous throw, source error и malformed response всегда превращаются в domain error. Fallback допустим только для валидного доменного исхода, явно представленного dependency contract, например корректного отсутствия данных. Fallback не используется для поглощения technical failure или contract drift.
## Чистый builder
`compositions/business/{domain}/create-{domain}-business.ts` только:
1. явно создаёт или получает runtime instances нужного lifecycle без I/O;
2. создаёт adapters поверх этих instances;
3. передаёт adapters и API других доменов в factory;
4. возвращает готовый `{Domain}Api`.
Builder не содержит:
- inline SDK calls;
- `window`/storage operations;
- domain mapping;
- domain errors;
- Zustand/SWR setup вперемешку с другими dependencies;
- product request при создании API;
- скрытую subscription без cleanup contract.
Если capability имеет lifecycle, business API предоставляет domain-level `start`/`subscribe` operation, которая использует dependency и возвращает cleanup wrapper. Business преобразует ошибки регистрации, callback и cleanup в собственные domain errors. Builder остаётся без side effects, а владелец scope запускает operation после mount/commit и вызывает cleanup при завершении lifecycle.
Browser/application builder без cross-domain dependencies вызывается без аргументов. Для request scope integration module определяет отдельный `requestScopeInput` только с framework/request data, например headers, cookies, request ID и abort signal. Concrete client factory импортируется и вызывается внутри integration module. Request input используется только для создания adapters, не передаётся в business как raw client и не экспортируется consumer compositions.
## Graph и lifecycle
Per-domain builder не является владельцем полного graph.
Владелец graph:
- выбирает application-lifetime composition/route/page/request/test scope;
- собирает домены в ацикличном порядке;
- создаёт ровно необходимый набор API;
- использует точный graph type;
- управляет cleanup subscriptions/resources;
- не повторяет adapter wiring;
- не импортирует raw infra для «досборки» домена.
Не используй generic `Partial<Business>` с последующим `as Business`. Если scope содержит только Auth, его contract должен обещать только Auth.
## Cross-domain dependencies
Один business-домен не импортирует runtime другого домена. Он объявляет нужную capability в своих `Deps` через type-only API другого домена, по возможности суженный `Pick`.
Готовый API передаётся builder-у при сборке graph:
```text
createAuthBusiness()
→ createUserBusiness({ authApi })
→ createOrdersBusiness({ userApi })
```
Runtime-цикл означает ошибочную границу доменов. Не скрывай цикл service locator, lazy import или глобальным event bus.
Подробный контракт фабрики находится в [Business-фабрике](./business-factory.md). Практическая сборка показана в [Business composition](../examples/business-composition.md).