Files
slm-design/old-docs/canons/business-runtime-boundary.md

294 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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).