Files
slm-design/DRAFT/level-2/domains/assemblies.md

265 lines
10 KiB
Markdown
Raw Normal View History

2026-08-02 22:53:05 +03:00
# Assemblies и production-граф
2026-08-02 22:53:05 +03:00
> Пояснение обязательной штатной сборки, дополнительных контекстов, environment compatibility и lifecycle.
## Связанные правила
- [`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)
2026-08-02 22:53:05 +03:00
- [`SLM-L2-API-A019`](../../rules/level-2.md#slm-l2-api-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)
2026-08-02 22:53:05 +03:00
- [`SLM-L2-API-A022`](../../rules/level-2.md#slm-l2-api-a022)
- [`SLM-L2-ASSEMBLY-R023`](../../rules/level-2.md#slm-l2-assembly-r023)
2026-08-02 22:53:05 +03:00
- [`SLM-L2-ASSEMBLY-R030`](../../rules/level-2.md#slm-l2-assembly-r030)
- [`SLM-L2-ASSEMBLY-R031`](../../rules/level-2.md#slm-l2-assembly-r031)
## Назначение
2026-08-02 22:53:05 +03:00
Assembly является SLM-модулем Group `assemblies`. Она выбирает production adapters своего домена, вызывает фабрики `api` и возвращает готовый именованный граф Domain API для одного объявленного production-контекста.
```text
2026-08-02 22:53:05 +03:00
api/factory + adapters + cross-domain APIs
→ assembly
→ named Domain API graph
```
2026-08-02 22:53:05 +03:00
Assembly не добавляет предметные методы, модели, transitions или ошибки. Она также не владеет framework state: готовый API передаётся framework binding или composition.
2026-08-02 22:53:05 +03:00
Импорт assembly не запускает side effects. Граф появляется только после вызова builder.
2026-08-02 22:53:05 +03:00
## Обязательная default assembly
2026-08-02 22:53:05 +03:00
Каждый пакет содержит модуль `assemblies/default`:
```text
auth/assemblies/
├── default/
│ └── index.ts
└── administration/
└── index.ts
```
`default` является штатной production-сборкой домена для одного baseline capability set, объявленного проектом. Она может быть browser-only, server-only, worker-compatible или действительно isomorphic. Имя не сообщает environment compatibility.
Пример metadata:
```yaml
assemblies:
default:
capabilities: [fetch, web-crypto]
conditions: [browser, import]
administration:
capabilities: [node, server-secrets]
conditions: [node, import]
```
Формат metadata не нормирован, но checker должен получать capability set и resolver conditions из явного project mapping, а не угадывать их по имени `default`.
## Дополнительные assemblies
Дополнительная assembly появляется, когда отличается реальная production-граница:
- набор Domain API;
- dependencies или providers;
- trust boundary;
- environment capabilities;
- scope или lifecycle;
- способ аутентификации;
- realtime guarantees.
Хорошие имена описывают контекст: `administration`, `realtime-session`, `worker`, `rsc`. Имя `rsc` оправдано только при отличающемся RSC wiring; само наличие Server Component не требует отдельной assembly.
Не создаётся assembly-заглушка с методами, бросающими `NOT_SUPPORTED`. Контекст возвращает только реально доступные API.
## Штатный граф
```ts
2026-08-02 22:53:05 +03:00
import type {
AuthSessionApi,
} from '@/domains/auth/api'
import {
createAuthSessionApi,
} from '@/domains/auth/api/factory'
import {
createAuthRestAdapter,
} from '@/domains/auth/adapters/identity-rest'
2026-08-02 22:53:05 +03:00
export type AuthGraph = Readonly<{
session: AuthSessionApi
}>
2026-08-02 22:53:05 +03:00
export const createAuth = (): AuthGraph => {
const session = createAuthSessionApi({
identity: createAuthRestAdapter(),
})
return { session }
}
```
2026-08-02 22:53:05 +03:00
Обычный graph owner импортирует только production builder:
```ts
import {
2026-08-02 22:53:05 +03:00
createAuth,
} from '@/domains/auth/assemblies/default'
2026-08-02 22:53:05 +03:00
const auth = createAuth()
```
2026-08-02 22:53:05 +03:00
Factory и concrete adapter остаются construction details assembly. Тесты API и adapters импортируют соответствующие границы напрямую.
2026-08-02 22:53:05 +03:00
## React + Vite и Next.js
В React + Vite `default` часто использует browser adapters:
```text
assemblies/default
→ browser REST adapter
→ browser WebSocket adapter
```
2026-08-02 22:53:05 +03:00
В Next.js та же `default` может считаться isomorphic только при совместимом executable graph под всеми заявленными conditions. Runtime branch не делает импорт безопасным:
```ts
2026-08-02 22:53:05 +03:00
// Недостаточное доказательство изоморфности.
if (typeof window === 'undefined') {
return createServerAdapter()
}
2026-08-02 22:53:05 +03:00
return createBrowserAdapter()
```
Если server и client требуют разных concrete dependencies, используются разные assemblies или framework-specific resolver entries, проверяемые отдельно.
## RSC boundary
RSC не переносит API instance с сервера в браузер:
```text
Server Component
→ request-scoped server assembly
→ server Domain API instance
→ public serializable value
→ Client Component boundary
→ separate client assembly
→ separate client Domain API instance
```
Server Component исполняется в server scope. Его импорт Client Component является framework reference, а не обычным executable edge RSC graph. При включённом SSR или prerender сам Client Component дополнительно исполняется в отдельном server render graph, а затем в browser hydration graph; обе фазы проверяются, а browser-only effects объявляются как framework-deferred edges. Server Action создаёт и очищает собственный request graph на каждый вызов.
Через boundary не передаются functions, API objects, ports, adapters, mutable cache clients или request secrets.
## Cross-domain input
Assembly зависимого домена принимает готовый API аргументом:
```ts
import type {
AuthSessionApi,
} from '@/domains/auth/api'
export type CreateUserInput = Readonly<{
auth: Pick<AuthSessionApi, 'getSession'>
}>
2026-08-02 22:53:05 +03:00
export const createUser = ({
auth,
2026-08-02 22:53:05 +03:00
}: CreateUserInput): UserGraph => {
const profile = createUserProfileApi({
auth,
2026-08-02 22:53:05 +03:00
profile: createUserProfileRestAdapter(),
})
return { profile }
}
```
2026-08-02 22:53:05 +03:00
Graph owner выполняет runtime-связь:
```ts
2026-08-02 22:53:05 +03:00
const auth = createAuth()
const user = createUser({
auth: auth.session,
})
```
2026-08-02 22:53:05 +03:00
User assembly делает только type-only импорт Auth API. Она не импортирует Auth factory, adapter или assembly. Общий runtime dependency graph остаётся ацикличным.
2026-08-02 22:53:05 +03:00
## Dependency-connected graph
2026-08-02 22:53:05 +03:00
Наличие `assemblies/default` у каждого Level 2 package не требует eager-сборки всех доменов:
2026-08-02 22:53:05 +03:00
```text
route A
→ auth/default
→ user/default
route B
→ catalog/default
```
2026-08-02 22:53:05 +03:00
Graph owner вызывает только builders, необходимые текущему scope. Module-level вызов `createAuth()` и global registry готовых APIs нарушают явное владение scope.
## Lifecycle
2026-08-02 22:53:05 +03:00
Factory не запускает запрос, socket, subscription или timer во время создания API. Явная операция, которая позже запускает ресурс, возвращает cleanup:
```ts
2026-08-02 22:53:05 +03:00
const subscription = await chat.subscribe(observer)
try {
2026-08-02 22:53:05 +03:00
await runScope()
} finally {
2026-08-02 22:53:05 +03:00
await subscription.close()
}
```
2026-08-02 22:53:05 +03:00
Если assembly создаёт owned resource или получает lifecycle handle adapter-owned resource, результат предоставляет aggregate cleanup:
```ts
2026-08-02 22:53:05 +03:00
export type ChatAssembly = Readonly<{
apis: ChatGraph
dispose: () => Promise<void>
}>
```
2026-08-02 22:53:05 +03:00
Cleanup является идемпотентным. После завершившегося cleanup resource не вызывает callbacks.
2026-08-02 22:53:05 +03:00
У каждого resource ровно один owner. Adapter, который сам создаёт connection или source cache, остаётся владельцем и экспортирует lifecycle handle; assembly только включает этот handle в aggregate cleanup. Если connection создаёт assembly, adapter получает borrowed capability и не закрывает её самостоятельно.
## Частичная ошибка сборки
Assembly регистрирует cleanup сразу после создания каждого owned resource и сразу после получения adapter lifecycle handle. Если следующий шаг завершается ошибкой, все зарегистрированные obligations выполняются до передачи ошибки caller-у:
```ts
export const createChat = async (): Promise<ChatAssembly> => {
const cleanups: Array<() => Promise<void>> = []
try {
const connection = await createRealtimeConnection()
cleanups.push(connection.close)
const history = createHistoryAdapter(connection)
const messages = createMessagesApi({ history })
return {
apis: { messages },
dispose: createIdempotentReverseCleanup(cleanups),
}
} catch (error) {
await runReverseCleanup(cleanups)
throw error
}
}
```
2026-08-02 22:53:05 +03:00
Реализация helper не нормирована. Нормативны достижимость cleanup на failure path, обратный dependency order и отсутствие callbacks после завершения disposal.
2026-08-02 22:53:05 +03:00
Assembly без cleanup obligations возвращает только API graph и не добавляет пустой `dispose` для симметрии. Наличие adapter-owned resource с переданным handle уже является cleanup obligation, даже если assembly не считается его владельцем.