mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
feat: Добавить VitePress
This commit is contained in:
293
old-docs/canons/business-runtime-boundary.md
Normal file
293
old-docs/canons/business-runtime-boundary.md
Normal 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).
|
||||
Reference in New Issue
Block a user