Files
slm-design/DRAFT/level-2/domains/state-cache.md

173 lines
9.1 KiB
Markdown
Raw Normal View History

2026-08-02 22:53:05 +03:00
# Состояние, cache и hydration
2026-08-02 22:53:05 +03:00
> Пояснение границы между семантической властью Domain API и framework-owned materialization.
## Связанные правила
2026-08-02 22:53:05 +03:00
- [`SLM-L2-API-R006`](../../rules/level-2.md#slm-l2-api-r006)
- [`SLM-L2-API-A007`](../../rules/level-2.md#slm-l2-api-a007)
- [`SLM-L2-DEPENDENCY-A012`](../../rules/level-2.md#slm-l2-dependency-a012)
- [`SLM-L2-FRAMEWORK-R015`](../../rules/level-2.md#slm-l2-framework-r015)
2026-08-02 22:53:05 +03:00
- [`SLM-L2-API-R018`](../../rules/level-2.md#slm-l2-api-r018)
- [`SLM-L2-STATE-R028`](../../rules/level-2.md#slm-l2-state-r028)
2026-08-02 22:53:05 +03:00
## Основная граница
2026-08-02 22:53:05 +03:00
Модуль `api` определяет форму и семантику доменных значений, но не выбирает способ их хранения и реактивной доставки. TanStack Query, SWR, Apollo, RTK Query, Zustand, Redux, MobX, Pinia, Signals и RxJS остаются в framework bindings или compositions.
2026-08-02 22:53:05 +03:00
```text
Domain API
→ public model/outcome/event
→ framework projection
→ rendering
```
Concrete state/query runtime не импортируется модулем `api`, не является dependency port фабрики и не входит в публичный Domain API.
## Виды materialization
2026-08-02 22:53:05 +03:00
### Source cache
2026-08-02 22:53:05 +03:00
Технический cache внешнего provider внутри adapter. Он может отвечать за transport deduplication, connection state, provider retry и хранение port records.
2026-08-02 22:53:05 +03:00
Source cache не публикует raw DTO, query keys, mutable client или library result через Domain API. Если adapter создаёт timers, subscriptions или connection, он остаётся единственным владельцем и экспортирует lifecycle handle, который assembly только агрегирует. Если resource создаёт assembly, adapter использует его как borrowed capability и не закрывает самостоятельно.
2026-08-02 22:53:05 +03:00
### Framework projection
State или cache, который framework binding строит из готового Domain API:
```ts
2026-08-02 22:53:05 +03:00
export const useAuthSession = () => {
const api = useAuthApi()
return useQuery({
queryKey: ['auth', 'session'],
queryFn: api.getSession,
})
}
```
2026-08-02 22:53:05 +03:00
Query key, stale time, pending/retry status, Suspense, rendering stale data и техническая invalidation принадлежат binding. `AuthSession` и `AuthError` принадлежат `api`.
2026-08-02 22:53:05 +03:00
### Composition state
2026-08-02 22:53:05 +03:00
Состояние конкретной страницы или multi-domain flow принадлежит composition: выбранная вкладка, открытый modal, draft формы, route transition и координация нескольких API.
2026-08-02 22:53:05 +03:00
Если draft приобретает самостоятельную доменную семантику, Domain API предоставляет validation или transition, но framework по-прежнему хранит возвращаемое readonly value.
2026-08-02 22:53:05 +03:00
## Domain API не является store
2026-08-02 22:53:05 +03:00
Публичный Domain API не экспортирует:
2026-08-02 22:53:05 +03:00
- mutable store;
- `getState` и `setState` framework runtime;
- QueryClient;
- Zustand `StoreApi`;
- framework hook;
- глобальный singleton данных;
- универсальный state port.
2026-08-02 22:53:05 +03:00
API methods возвращают значения и outcomes. Framework consumer решает, как долго их хранить и когда повторно запросить.
2026-08-02 22:53:05 +03:00
Это не означает, что framework определяет предметные transitions. Он материализует только то, что произвёл или проверил API.
## Invalidation и retry
| Политика | Обычный владелец |
|---|---|
2026-08-02 22:53:05 +03:00
| Query key, stale time, deduplication, background refetch | Framework binding |
| Transport retry безопасного запроса | Adapter |
2026-08-02 22:53:05 +03:00
| Rendering stale data, Suspense, polling UI | Framework binding или composition |
| Запрет повторной предметной команды | Domain API |
| Cooldown, лимит попыток, допустимый transition | Domain API |
| Freshness, влияющая на корректность сценария | Domain API через operation contract |
2026-08-02 22:53:05 +03:00
После успешной команды binding может технически invalidировать известные query keys. Если выбор invalidation выражает предметную семантику, Domain API возвращает устойчивый outcome/event, а binding только отображает его на framework cache.
## Optimistic updates
2026-08-02 22:53:05 +03:00
Framework binding не конструирует произвольную публичную модель из form input, raw DTO или текущего cache. Optimistic projection допустима, когда предполагаемое значение:
2026-08-02 22:53:05 +03:00
- возвращено командой Domain API;
- создано отдельной операцией Domain API;
- создано или проверено pure-функцией `api/runtime`.
```ts
2026-08-02 22:53:05 +03:00
import {
projectProfileUpdate,
} from '@/domains/user/api/runtime'
const optimisticProfile = projectProfileUpdate(
currentProfile,
command,
)
queryClient.setQueryData(profileKey, optimisticProfile)
```
2026-08-02 22:53:05 +03:00
`projectProfileUpdate` владеет предметным transition, а `setQueryData` остаётся framework operation.
## Concurrent mutations и realtime
При нескольких optimistic commands и realtime events binding не выбирает самостоятельно ordering, versioning, rollback или rebase. Domain API возвращает correlation/version metadata либо предоставляет deterministic reconciliation:
```ts
const nextProjection = reconcileProfile({
current,
event,
pendingCommands,
})
```
Если API не объявляет безопасный merge, binding invalidates cache и получает authoritative snapshot через Domain API. Это предпочтительнее скрытого применения неполного delta.
## Persistence
Framework cache может технически сохраняться между reloads, но persisted value не становится источником предметной истины. После восстановления значение:
- используется как stale projection до revalidation;
- либо проверяется публичным validator `api/runtime`;
- либо отбрасывается и загружается через Domain API.
Если storage является самостоятельным предметным внешним источником, доступ к нему оформляется dependency port и adapter. Автоматический framework middleware не обходит API validation и transitions.
## SSR и hydration
Server и client имеют разные API instances и caches:
```text
server request
→ request assembly
→ server Domain API
→ server framework cache
→ serializable hydration payload
browser
→ client assembly
→ client Domain API
→ hydrated client cache
```
Hydration payload принадлежит framework binding и содержит только public domain values и framework metadata. API object, functions, ports, adapters, mutable clients и request secrets не сериализуются.
2026-08-02 22:53:05 +03:00
Server cache создаётся на каждый request и не хранится в module singleton. Client cache создаётся на согласованный application или route scope.
2026-08-02 22:53:05 +03:00
## RSC и Server Actions
2026-08-02 22:53:05 +03:00
Server Component вызывает server Domain API и передаёт Client Component только сериализуемые values или hydration payload. Client Component создаёт или получает отдельный client API instance; при SSR его render отдельно проверяется в server prerender graph до browser hydration.
2026-08-02 22:53:05 +03:00
Server Action создаёт request-scoped production graph на каждый вызов, выполняет Domain API command и гарантированно выполняет все cleanup obligations графа. Client invocation Server Action является framework reference edge, а не передачей server API в browser.
## Проверка на ревью
Для каждого state/query runtime определяется:
2026-08-02 22:53:05 +03:00
- является ли он source cache, framework projection или composition state;
- откуда поступают public domain values;
- кто определяет validation и transition;
- где находятся library-specific types и keys;
2026-08-02 22:53:05 +03:00
- как invalidation связана с Domain API outcomes;
- как обрабатываются optimistic concurrency и realtime events;
- что сериализуется при SSR/RSC;
- соответствует ли cache scope области жизни API graph.