8.4 KiB
Модуль business
Пояснение единственного runtime-источника доменных данных и результатов.
Связанные правила
SLM-L2-BUSINESS-R005SLM-L2-BUSINESS-R006SLM-L2-BUSINESS-A007SLM-L2-FACTORY-R008SLM-L2-ERROR-R009SLM-L2-ERROR-R010SLM-L2-BUSINESS-R018SLM-L2-BUSINESS-A019SLM-L2-BUSINESS-A022
Роль
business является обязательным SLM-модулем доменного пакета. Он владеет:
- публичными предметными сценариями;
- единым контрактом
DomainApi; - одной публичной фабрикой;
- типом явных зависимостей фабрики;
- предметными типами и детерминированными правилами;
- кодами, типом и runtime guard доменных ошибок;
- публичным представлением доменных данных и состояния.
Приложение получает runtime-данные, состояние и результаты домена только через экземпляр DomainApi. Adapter, preset или framework binding module не открывает параллельный источник доменных данных.
Публичный API модуля
Один логический API business разделён на три фиксированных фасета.
Type-only barrel
Корневой business/index.ts экспортирует только типы:
export type {
AuthApi,
AuthDeps,
AuthError,
AuthErrorCode,
AuthFactory,
AuthState,
} from './types'
Потребитель использует этот путь только через import type:
import type {
AuthApi,
AuthError,
AuthErrorCode,
} from '@/domains/auth/business'
Factory entry
business/factory.ts экспортирует только runtime-фабрику:
export { authFactory } from './auth.factory'
import { authFactory } from '@/domains/auth/business/factory'
Error entry
business/error.ts экспортирует только runtime-коды и guards:
export { AUTH_ERROR_CODES, isAuthError } from './errors/auth-error'
import {
AUTH_ERROR_CODES,
isAuthError,
} from '@/domains/auth/business/error'
AuthError и AuthErrorCode не реэкспортируются из business/error: все public types имеют один канонический путь через type-only barrel. Предметные validators, normalizers, constructors ошибок, source-error mappers, mutable store и технические DTO остаются закрытыми.
Другие внешние пути внутри business являются deep imports. Файлы factory.ts и error.ts являются фасетами одного SLM-модуля, а не сегментами или вложенными модулями.
Потребители фасетов
| Потребитель | business |
business/factory |
business/error |
|---|---|---|---|
| Adapter module своего домена | Type-only | Нет | Нет |
| Preset своего домена | Type-only | Да | Нет |
| Framework binding module своего домена | Type-only | Нет | Да |
composition или app |
Type-only | Да | Да |
| Модуль другого доменного пакета | Type-only | Нет | Нет |
| Тест | Type-only | По границе тестируемого владельца | По границе тестируемого владельца |
Один DomainApi
export type AuthApi = {
getSnapshot: () => AuthState
requestPhoneOtp: (phone: string) => Promise<void>
verifyPhoneOtp: (code: string) => Promise<void>
}
export type AuthFactory = (deps: AuthDeps) => AuthApi
Все presets вызывают одну authFactory и создают AuthApi этого контракта. Preset может использовать другую техническую реализацию, но не добавляет метод и не меняет семантику сценария. Одноразовое место сборки в composition также может вызвать business/factory, используя публичные adapter-модули пакета, если фабрика имеет технические зависимости.
Точная модель хранения, initial state, подписки и SSR snapshot пока остаётся открытым вопросом. Нормативной уже является публичная граница: приложение наблюдает доменное состояние через DomainApi, а не напрямую через adapter или framework store.
Обязательный контракт ошибок
Каждый business объявляет устойчивые коды, безопасную readonly-форму и runtime guard. Типы публикуются через business, а runtime symbols через business/error:
export const AUTH_ERROR_CODES = {
PHONE_INVALID: 'AUTH_PHONE_INVALID',
OTP_REQUEST_FAILED: 'AUTH_OTP_REQUEST_FAILED',
OTP_CODE_INVALID: 'AUTH_OTP_CODE_INVALID',
} as const
export type AuthErrorCode =
typeof AUTH_ERROR_CODES[keyof typeof AUTH_ERROR_CODES]
export type AuthError = Readonly<{
code: AuthErrorCode
}>
const authErrorCodes = new Set<string>(Object.values(AUTH_ERROR_CODES))
export const isAuthError = (value: unknown): value is AuthError => {
if (typeof value !== 'object' || value === null) {
return false
}
const prototype = Object.getPrototypeOf(value)
const keys = Reflect.ownKeys(value)
if (
(prototype !== Object.prototype && prototype !== null)
|| keys.length !== 1
|| keys[0] !== 'code'
|| !('code' in value)
) {
return false
}
return typeof value.code === 'string' && authErrorCodes.has(value.code)
}
Способ передачи ошибки, exception или discriminated Result, пока не закреплён. В обоих вариантах публичный сценарий сообщает ожидаемый сбой только через собственный AuthError.
Изоляция технических ошибок
Ошибки SDK, HTTP, database, storage и adapters не пересекают DomainApi в форме, доступной приложению. business преобразует обрабатываемый технический сбой в собственный код:
SDK error
→ adapter failure
→ business mapping
→ AuthErrorCode
→ приложение
Публичная форма не включает исходные message, status, payload, cause, SDK class или сам объект ошибки. Диагностические данные могут сохраняться только во внутреннем механизме observability, контракт которого будет определён отдельно.
То же относится к cross-domain вызову. Если UserApi использует AuthApi, сбой, который становится результатом публичного сценария User, представлен собственным UserError, а не AuthError.
Ошибки программирования и нарушенные внутренние инварианты не обязаны маскироваться под ожидаемые доменные ошибки. Способ отличить их от ожидаемых cross-domain ошибок при exception-модели и их диагностическая политика остаются открытыми вопросами; исходная ошибка при этом не добавляется в публичную форму DomainError.