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

7.7 KiB
Raw Blame History

Assemblies и среды выполнения

Пояснение повторяемой сборки именованного графа Domain API.

Связанные правила

Назначение

Assembly является SLM-модулем в Group assemblies. Она создаёт явный граф одного или нескольких Domain API пакета для конкретного повторяемого контекста выполнения. Каждый доменный пакет содержит минимум одну assembly.

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:

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, которого нет в браузере:

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:

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-связь:

const auth = createAuthForRequest(authInput)
const user = createUserForRequest({
  auth: auth.session,
  request: userInput,
})

Environment entry points

Server assembly имеет отдельный публичный entry point и marker выбранного framework или bundler:

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:

const stop = auth.session.startInvalidationTracking()

try {
  // Scope использует API.
} finally {
  await stop()
}

Если assembly сама создаёт ресурс, принадлежащий всему возвращённому графу, результат дополнительно предоставляет cleanup handle:

export type AuthRequestAssembly = Readonly<{
  apis: AuthRequestGraph
  dispose: () => Promise<void>
}>
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 остаётся открытым вопросом.