mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
171 lines
7.7 KiB
Markdown
171 lines
7.7 KiB
Markdown
# Assemblies и среды выполнения
|
||
|
||
> Пояснение повторяемой сборки именованного графа Domain API.
|
||
|
||
## Связанные правила
|
||
|
||
- [`SLM-L2-FACTORY-R008`](../../rules/level-2.md#slm-l2-factory-r008)
|
||
- [`SLM-L2-ASSEMBLY-R011`](../../rules/level-2.md#slm-l2-assembly-r011)
|
||
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
|
||
- [`SLM-L2-ENVIRONMENT-A013`](../../rules/level-2.md#slm-l2-environment-a013)
|
||
- [`SLM-L2-BUSINESS-A019`](../../rules/level-2.md#slm-l2-business-a019)
|
||
- [`SLM-L2-ASSEMBLY-A020`](../../rules/level-2.md#slm-l2-assembly-a020)
|
||
- [`SLM-L2-ADAPTER-R021`](../../rules/level-2.md#slm-l2-adapter-r021)
|
||
- [`SLM-L2-BUSINESS-A022`](../../rules/level-2.md#slm-l2-business-a022)
|
||
- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023)
|
||
|
||
## Назначение
|
||
|
||
Assembly является SLM-модулем в Group `assemblies`. Она создаёт явный граф одного или нескольких Domain API пакета для конкретного повторяемого контекста выполнения. Каждый доменный пакет содержит минимум одну assembly.
|
||
|
||
```text
|
||
business/factory
|
||
├── assemblies/browser → { session: AuthSessionApi }
|
||
├── assemblies/request → { session, administration }
|
||
└── assemblies/server-action → { administration }
|
||
```
|
||
|
||
Архитектура не требует `base` или изоморфную assembly и не ограничивает их максимальное количество. Обязательная assembly соответствует реальному поддерживаемому контексту, а не существует только для заполнения структуры.
|
||
|
||
Место сборки графа в `composition` может вызвать фабрики напрямую для одноразовой конфигурации. Такая сборка не отменяет обязательную assembly пакета и при наличии технических зависимостей использует публичные adapter-модули, а не inline implementations.
|
||
|
||
## Именованный граф API
|
||
|
||
Browser assembly импортирует только фабрики и adapters нужных ей API:
|
||
|
||
```ts
|
||
import type { AuthSessionApi } from '@/domains/auth/business'
|
||
import { authSessionFactory } from '@/domains/auth/business/factory'
|
||
import { createPhoneHttpAdapter } from '@/domains/auth/adapters/phone-http'
|
||
import { createBrowserSessionAdapter } from '@/domains/auth/adapters/browser-session'
|
||
|
||
export type AuthBrowserGraph = Readonly<{
|
||
session: AuthSessionApi
|
||
}>
|
||
|
||
export const createBrowserAuth = (): AuthBrowserGraph => {
|
||
const session = authSessionFactory({
|
||
phone: createPhoneHttpAdapter(),
|
||
session: createBrowserSessionAdapter(),
|
||
})
|
||
|
||
return { session }
|
||
}
|
||
```
|
||
|
||
Request assembly может собрать дополнительный API, которого нет в браузере:
|
||
|
||
```ts
|
||
import type {
|
||
AuthAdministrationApi,
|
||
AuthSessionApi,
|
||
} from '@/domains/auth/business'
|
||
|
||
import {
|
||
authAdministrationFactory,
|
||
authSessionFactory,
|
||
} from '@/domains/auth/business/factory'
|
||
|
||
export type AuthRequestGraph = Readonly<{
|
||
administration: AuthAdministrationApi
|
||
session: AuthSessionApi
|
||
}>
|
||
```
|
||
|
||
Assembly не добавляет методы к этим контрактам и не создаёт общий `AuthApi`. Именованный объект только сообщает, какие независимые API доступны в контексте.
|
||
|
||
## Cross-domain input
|
||
|
||
Assembly зависимого домена принимает готовый API аргументом. Cross-domain API не является adapter и не размещается в Group `adapters`:
|
||
|
||
```ts
|
||
import type { AuthSessionApi } from '@/domains/auth/business'
|
||
import type { UserProfileApi } from '@/domains/user/business'
|
||
import { userProfileFactory } from '@/domains/user/business/factory'
|
||
import { createUserProfileAdapter } from '@/domains/user/adapters/profile'
|
||
|
||
export type CreateUserForRequestInput = {
|
||
auth: Pick<AuthSessionApi, 'getSnapshot'>
|
||
request: UserRequestInput
|
||
}
|
||
|
||
export type UserRequestGraph = Readonly<{
|
||
profile: UserProfileApi
|
||
}>
|
||
|
||
export const createUserForRequest = ({
|
||
auth,
|
||
request,
|
||
}: CreateUserForRequestInput): UserRequestGraph => {
|
||
const profile = userProfileFactory({
|
||
auth,
|
||
profile: createUserProfileAdapter(request),
|
||
})
|
||
|
||
return { profile }
|
||
}
|
||
```
|
||
|
||
Assembly делает только type-only импорт `AuthSessionApi`. Runtime-фабрику, assembly или instance Auth она не импортирует.
|
||
|
||
Место сборки графа выполняет runtime-связь:
|
||
|
||
```ts
|
||
const auth = createAuthForRequest(authInput)
|
||
const user = createUserForRequest({
|
||
auth: auth.session,
|
||
request: userInput,
|
||
})
|
||
```
|
||
|
||
## Environment entry points
|
||
|
||
Server assembly имеет отдельный публичный entry point и marker выбранного framework или bundler:
|
||
|
||
```ts
|
||
import 'server-only'
|
||
|
||
export { createAuthForRequest } from './create-auth-for-request'
|
||
```
|
||
|
||
Server entry point не реэкспортируется через `business`, Framework Group, browser assembly или корень доменного пакета. Аналогично client-only код не достигается из server/shared entry point, если выбранная среда запрещает такую зависимость.
|
||
|
||
## Lifecycle
|
||
|
||
Factory не запускает запрос, subscription, timer или другую скрытую долгоживущую работу во время создания API. Assembly может активировать технический ресурс только с явной передачей cleanup своему caller. Операция Domain API, которая запускает ресурс позже, сама возвращает cleanup:
|
||
|
||
```ts
|
||
const stop = auth.session.startInvalidationTracking()
|
||
|
||
try {
|
||
// Scope использует API.
|
||
} finally {
|
||
await stop()
|
||
}
|
||
```
|
||
|
||
Если assembly сама создаёт ресурс, принадлежащий всему возвращённому графу, результат дополнительно предоставляет cleanup handle:
|
||
|
||
```ts
|
||
export type AuthRequestAssembly = Readonly<{
|
||
apis: AuthRequestGraph
|
||
dispose: () => Promise<void>
|
||
}>
|
||
```
|
||
|
||
```ts
|
||
const auth = createAuthForRequest(input)
|
||
|
||
try {
|
||
return await handleRequest(auth.apis)
|
||
} finally {
|
||
await auth.dispose()
|
||
}
|
||
```
|
||
|
||
Assembly без собственного ресурса не обязана возвращать пустой `dispose`. Graph owner вызывает каждый реально предоставленный cleanup не позже завершения application, route, request или test scope.
|
||
|
||
Одноразовое место сборки, которое вызывает фабрики напрямую, подчиняется той же границе: оно не запускает скрытый ресурс в constructor и сохраняет cleanup любого созданного adapter-ресурса до завершения своего scope.
|
||
|
||
Рекомендуется делать `dispose` идемпотентным, освобождать частично созданные ресурсы при ошибке assembly и закрывать зависимые ресурсы раньше их зависимостей. Детальная политика rollback и поведения API после cleanup остаётся открытым вопросом.
|