feat: 기능 추가 과정중
This commit is contained in:
@@ -0,0 +1,802 @@
|
||||
# VD-25: Server State Cache lifecycle
|
||||
|
||||
- 상태: Accepted — reference bound-query/input-aware mutation baseline composed, lifecycle delta pending
|
||||
- 결정일: 2026-07-28
|
||||
- TanStack Query memory runtime: `COMPOSED`
|
||||
- reference bound-query/profile/input-aware duplicate coordination: `COMPOSED`
|
||||
- session-generation/identity lifecycle: `COMPOSED`
|
||||
- conditional sidecar/optimistic layer/cursor runtime: `AVAILABLE_NOT_COMPOSED`
|
||||
- account projection/infinite-query/effect reconciliation delta:
|
||||
`DESIGNED_NOT_IMPLEMENTED`
|
||||
- normalized graph cache: `NOT_SELECTED`
|
||||
- query persistence product selection: `NOT_SELECTED`
|
||||
- 관련 결정: VD-13, VD-23, VD-24, VD-26, VD-27, VD-29, VD-30
|
||||
- 상세 설계:
|
||||
[API contract, Schema, Mapper와 Server State](../api-contract-schema-mapper-and-server-state.md)
|
||||
|
||||
## 배경
|
||||
|
||||
현재 production bootstrap은 runtime별 QueryClient와 invalidation coordinator를
|
||||
실제로 조립한다. reference presentation은 application input을
|
||||
`useApplicationQuery()`/`useApplicationMutation()`에 연결하며 다음을 제공한다.
|
||||
|
||||
- AbortSignal cancellation
|
||||
- finite global stale/gc default
|
||||
- Query network retry off
|
||||
- stale-degraded 표시
|
||||
- exact-key optimistic snapshot/rollback
|
||||
- conflict 표면
|
||||
- mutation topic lease와 cross-context invalidate-only hint
|
||||
|
||||
reference list/detail은 bound definition이 key, executor와 profile을 함께
|
||||
제공한다. strict canonical codec은 depth/node/string/encoded-byte, cycle/shared
|
||||
reference, undefined/NaN/negative-zero, sparse array, accessor와 non-plain object를
|
||||
닫고 runtime-private opaque identity만 query key에 넣는다. profile의 stale/gc/
|
||||
refetch/retry owner와 result item/byte admission이 실제 hook에 적용된다.
|
||||
|
||||
current invalidation definition은 namespace당 singular topic이고 같은 topic의
|
||||
multi-namespace fan-out을 compose하지 못한다. 아래 bounded topic-set registry는
|
||||
target delta이며 현재 runtime 증거가 아니다.
|
||||
|
||||
production scope runtime은 session transition 즉시 old generation을 fence하고
|
||||
QueryClient cancel/clear 뒤 새 opaque scope를 발급한다. identity registry는 active
|
||||
lease, refcount, bounded LRU, canonical-byte/entry ceiling과 token collision 검사를
|
||||
scope별로 소유한다. mutation duplicate baseline도 scope exact semantic input만
|
||||
join하고 late result를 폐기한다.
|
||||
|
||||
ordered optimistic layer, conditional validator CAS sidecar와 bounded cursor chain은
|
||||
실행 가능한 test runtime까지 존재하지만 reference backend/definition에는 아직
|
||||
연결하지 않았다. 남은 부분은 account identity projection, logical-key serialization,
|
||||
HTTP 304 transaction, product optimistic membership/revision, infinite-query binding과
|
||||
effect certainty reconcile이다.
|
||||
|
||||
## 소유권
|
||||
|
||||
VD-13:
|
||||
|
||||
- `CacheScopeSnapshot`
|
||||
- ORIGIN/ACCOUNT/SESSION projection
|
||||
- QueryClient generation/fence/remount
|
||||
- strict canonical key codec 공통 구현
|
||||
- cross-tab wire와 durable namespace epoch
|
||||
- optional IndexedDB persistence와 restore
|
||||
- logout/account switch purge
|
||||
|
||||
VD-25:
|
||||
|
||||
- operation/application query definition binding
|
||||
- per-query freshness/gc/refetch/result budget
|
||||
- cache admission과 mapped value contract
|
||||
- cursor/infinite pagination
|
||||
- conditional revalidation integration
|
||||
- mutation concurrency, optimistic patch와 reconciliation
|
||||
- invalidation/seed policy
|
||||
- transport-independent error/stale behavior
|
||||
|
||||
VD-25는 VD-13의 scope snapshot/key codec을 소비하고 다른 epoch/fingerprint
|
||||
protocol을 만들지 않는다.
|
||||
|
||||
## 결정
|
||||
|
||||
### 1. TanStack Query가 유일한 기본 Server State owner다
|
||||
|
||||
REST, GraphQL과 gRPC-Web unary result는 transport-independent application
|
||||
projection으로 mapping된 뒤 TanStack Query memory cache에 들어갈 수 있다.
|
||||
|
||||
기본적으로 설치하지 않는다.
|
||||
|
||||
- Redux/Zustand server entity copy
|
||||
- Apollo/urql normalized cache
|
||||
- raw HTTP response cache wrapper
|
||||
- generated client SDK cache
|
||||
- custom Map singleton
|
||||
|
||||
GraphQL normalized cache가 실제로 필요하면 bounded context에서 TanStack
|
||||
operation-result cache를 대체하는 별도 ADR을 승인한다. 두 cache에 같은 entity를
|
||||
동시 write하지 않는다.
|
||||
|
||||
browser HTTP cache/Cache Storage, TanStack Query memory와 IndexedDB query
|
||||
persistence는 서로 다른 owner다.
|
||||
|
||||
### 2. Bound Query Definition
|
||||
|
||||
caller가 query key와 executor를 독립적으로 조립하지 않는다.
|
||||
|
||||
```text
|
||||
QueryDefinition<Input, Value>
|
||||
definitionId
|
||||
owner
|
||||
operationId
|
||||
inputCodec
|
||||
keyCodecId
|
||||
serverStateProfileId
|
||||
scopePersistencePolicyId
|
||||
resultContractId
|
||||
execute(validatedInput, executionContext)
|
||||
```
|
||||
|
||||
binding:
|
||||
|
||||
```text
|
||||
bindQuery(definition, rawInput, CacheScopeSnapshot)
|
||||
-> validate/canonicalize input
|
||||
-> derive branded query key
|
||||
-> resolve immutable policy
|
||||
-> freeze execute closure and captured generation
|
||||
-> BoundQuery<Value>
|
||||
```
|
||||
|
||||
presentation API:
|
||||
|
||||
```text
|
||||
useApplicationQuery(boundQuery)
|
||||
```
|
||||
|
||||
`queryKey`, `queryFn`, stale/gc/retry와 arbitrary TanStack option을 feature page에서
|
||||
따로 넘기지 않는다. escape hatch가 필요하면 새 profile을 먼저 등록한다.
|
||||
|
||||
### 3. Semantic query identity
|
||||
|
||||
query key는 VD-13의 단일 normative layout을 그대로 사용한다.
|
||||
|
||||
```text
|
||||
[
|
||||
"query",
|
||||
keySchemaVersion,
|
||||
scopeProjectionFingerprint,
|
||||
namespaceName,
|
||||
namespaceVersion,
|
||||
queryDefinitionVersion,
|
||||
canonicalSemanticInput
|
||||
]
|
||||
```
|
||||
|
||||
- namespace/key schema/version과 scope projection은 VD-13 profile-owned
|
||||
- query definition version과 semantic input projection은 VD-25 definition-owned
|
||||
- scope projection은 VD-13의 exact profile output
|
||||
- input은 strict codec의 plain immutable representation
|
||||
- REST URL/query string, ETag와 cursor raw value를 root identity에 넣지 않음
|
||||
- GraphQL document/persisted hash를 넣지 않음
|
||||
- protobuf bytes/generated message를 넣지 않음
|
||||
- presentation locale/formatted string을 넣지 않음
|
||||
|
||||
transport migration이 use-case/result meaning을 보존하면 semantic query family를
|
||||
유지할 수 있다. schema/mapper meaning, scope나 output identity가 바뀌면
|
||||
query-definition/release epoch를 바꾼다.
|
||||
|
||||
strict key codec은 다음을 거절한다.
|
||||
|
||||
- `undefined`, sparse array
|
||||
- NaN, Infinity, negative zero policy mismatch
|
||||
- bigint/symbol/function
|
||||
- Date/Map/Set/RegExp/typed array/native/class instance
|
||||
- accessor/proxy/prototype pollution key
|
||||
- cycle/shared-reference ambiguity
|
||||
- non-plain object
|
||||
- depth/node/part/string/encoded-byte ceiling 초과
|
||||
|
||||
query key에 PII/business ID를 직접 넣지 않는다. 필요한 resource identity는
|
||||
feature policy가 발급한 opaque bounded token으로 투영한다.
|
||||
|
||||
cursor와 command 동일성은 raw value나 충돌 가능 digest만으로 판정하지 않는다.
|
||||
|
||||
```text
|
||||
RuntimeIdentityTokenCodecV1
|
||||
canonicalCodecVersion
|
||||
maxCanonicalBytes
|
||||
maxInternEntries
|
||||
maxInternCanonicalBytes
|
||||
tokenEntropyBits >= 128
|
||||
lifetime = RUNTIME_SCOPE
|
||||
```
|
||||
|
||||
- strict length-prefixed typed canonical encoding이 exact equality source다.
|
||||
- runtime-private intern table이 canonical bytes를 opaque random token에
|
||||
일대일로 binding하고 token collision을 reverse map으로 검사한다.
|
||||
- 같은 token 후보가 다른 canonical bytes와 충돌하면 새 token을 발급한다. bounded
|
||||
재시도 후에도 해결되지 않으면 `IDENTITY_TOKEN_COLLISION`으로 admission/join을
|
||||
fail-closed한다.
|
||||
- intern row는 lease/refcount를 가진다. Query entry가 설치된 동안, active
|
||||
observer/fetch와 mutation/join이 진행되는 동안 해당 token을 eviction하지 않는다.
|
||||
- Query removal/GC에서 query token lease를, mutation terminal/join waiter
|
||||
settlement에서 command token lease를 exact once release한다. runtime/scope
|
||||
close는 남은 table을 전부 폐기한다.
|
||||
- refcount 0 row만 bounded LRU eviction할 수 있다. entry 수 또는 total canonical
|
||||
bytes ceiling을 active lease 때문에 회수할 수 없으면
|
||||
`IDENTITY_INTERN_LIMIT_EXCEEDED`로 신규 cache/command admission을 fail-closed한다.
|
||||
- canonical bytes/raw cursor/command input은 query key, diagnostics,
|
||||
cross-context wire와 persistence에 넣지 않고 runtime/scope close에서 폐기한다.
|
||||
- token은 backend idempotency key, authorization proof나 durable identity가 아니다.
|
||||
|
||||
### 4. ServerStateProfile
|
||||
|
||||
```text
|
||||
ServerStateProfileV1
|
||||
profileId
|
||||
classification
|
||||
scopePersistencePolicyId
|
||||
staleTimeMs
|
||||
gcTimeMs
|
||||
refetchOnMount
|
||||
refetchOnFocus
|
||||
refetchOnReconnect
|
||||
networkMode
|
||||
retryOwner = TRANSPORT | QUERY | NONE
|
||||
maxResultItems
|
||||
maxEstimatedResultBytes
|
||||
paginationProfileId | null
|
||||
conditionalProfileId | null
|
||||
placeholderPolicy
|
||||
initialFailurePolicy
|
||||
refreshFailurePolicy
|
||||
authorizationFailurePolicy
|
||||
contractFailurePolicy
|
||||
invalidationTopicRefs[] = { topicId, topicVersion }
|
||||
owner
|
||||
```
|
||||
|
||||
implementation ceilings:
|
||||
|
||||
- query `invalidationTopicRefs` set/version은 joined VD-13
|
||||
`QueryScopePersistencePolicy`와 exact match
|
||||
- `gcTimeMs`는 inactive retention이고 `staleTimeMs`는 freshness이므로
|
||||
`staleTimeMs <= gcTimeMs`를 일반 불변조건으로 강제하지 않는다.
|
||||
- VD-13 persistence를 선택한 경우에만 restore `maxAgeMs`, retention과
|
||||
`gcTimeMs`의 join compatibility를 검증한다.
|
||||
- finite gc 기본, `Infinity`는 explicit immortal-static profile만
|
||||
- stale/gc 최대값
|
||||
- result item/estimated byte 상한
|
||||
- maximum pages
|
||||
- refetch trigger storm coalescing
|
||||
- foreground/background concurrency
|
||||
|
||||
현재 global 30초/5분은 reference default이지 모든 product query의 production
|
||||
정책이 아니다.
|
||||
|
||||
### 5. Retry owner
|
||||
|
||||
한 network operation에는 retry owner가 정확히 하나다.
|
||||
|
||||
| 상황 | 기본 owner |
|
||||
| --- | --- |
|
||||
| installed REST | REST transport |
|
||||
| persisted GraphQL | GraphQL transport |
|
||||
| Connect unary | Connect adapter 또는 selected edge 중 exact one |
|
||||
| gRPC-Web unary | gRPC-Web transport |
|
||||
| pure local query computation | Query 또는 none |
|
||||
|
||||
transport retry가 있는 definition은 TanStack `retry=false`다. Query retry callback이
|
||||
same gateway call을 다시 실행해 transport attempts를 배가하지 않는다.
|
||||
|
||||
manual UI retry는 새 logical query execution이다. keyed command의 ambiguous outcome을
|
||||
query retry처럼 재실행하지 않는다.
|
||||
|
||||
### 6. Cache admission
|
||||
|
||||
query cache에 admission 가능한 값:
|
||||
|
||||
- VD-24 mapper가 만든 immutable plain application read model
|
||||
- exact result contract/version
|
||||
- current scope/runtime generation
|
||||
- item/estimated-byte ceiling 안
|
||||
- complete result 또는 operation이 허용한 typed partial result
|
||||
|
||||
금지:
|
||||
|
||||
- raw JSON/GraphQL envelope
|
||||
- generated protobuf message/client
|
||||
- Response/ReadableStream
|
||||
- auth/CSRF/idempotency/cursor/validator/trace metadata
|
||||
- thrown Error/AppFailure detail payload
|
||||
- function/class/domain service/native object
|
||||
|
||||
admission 순서:
|
||||
|
||||
```text
|
||||
transport success
|
||||
-> schema
|
||||
-> mapper
|
||||
-> result budget
|
||||
-> scope/generation fence
|
||||
-> Query commit
|
||||
```
|
||||
|
||||
어느 단계든 실패하면 cache write 0회다.
|
||||
|
||||
### 7. Result size
|
||||
|
||||
`maxEstimatedResultBytes`는 memory reservation이 아니라 hard admission guard다.
|
||||
|
||||
- mapper output plain data를 bounded estimator로 측정
|
||||
- string UTF-8 bytes, key overhead, array/object node count를 보수적으로 합산
|
||||
- cycle/class/native/accessor는 측정 전에 거절
|
||||
- 측정 자체가 deadline/node ceiling을 넘으면 admission 거절
|
||||
- result cap을 넘겨도 transport success를 unbounded UI state로 반환하지 않고
|
||||
`RESULT_LIMIT_EXCEEDED` 또는 server pagination requirement로 닫음
|
||||
|
||||
large binary/collection은 streaming/file 또는 cursor page capability로 이동한다.
|
||||
|
||||
### 8. Query lifecycle
|
||||
|
||||
```text
|
||||
IDLE
|
||||
-> LOADING
|
||||
-> SUCCESS | EMPTY | TERMINAL_ERROR
|
||||
|
||||
SUCCESS | EMPTY
|
||||
-> REFRESHING
|
||||
-> SUCCESS | EMPTY
|
||||
-> STALE_DEGRADED
|
||||
-> TERMINAL/REAUTH when policy forbids stale visibility
|
||||
```
|
||||
|
||||
current AsyncState의 base와 overlay 구분을 유지한다.
|
||||
|
||||
failure policy:
|
||||
|
||||
| failure | default |
|
||||
| --- | --- |
|
||||
| transient network/5xx refresh failure | valid previous data + stale-degraded |
|
||||
| caller/navigation abort | terminal error로 표시하지 않음 |
|
||||
| auth required | sensitive/account query는 stale 숨김, reauth |
|
||||
| forbidden/account switch | current scope data 즉시 숨김/clear |
|
||||
| schema/mapper/contract mismatch | cache write 금지, default stale 숨김 또는 explicit safe-static exception |
|
||||
| rate limit | previous data 정책 + retry-after UX |
|
||||
| not found | feature policy에 따라 empty/remove/tombstone |
|
||||
|
||||
query profile이 sensitive stale data를 계속 보여 주는 결정을 global fallback으로
|
||||
상속하지 않는다.
|
||||
|
||||
### 9. Freshness와 refetch
|
||||
|
||||
`staleTime`은 business correctness/authorization TTL이 아니다.
|
||||
|
||||
- focus/reconnect/mount refetch는 profile별
|
||||
- simultaneous trigger는 one in-flight query로 coalesce
|
||||
- minimum refetch interval과 deadline 적용
|
||||
- visibility offline state는 hint이며 server revision을 대체하지 않음
|
||||
- response age/cache-control을 staleTime으로 자동 변환하지 않음
|
||||
- backend push/invalidation은 stale hint이며 authoritative refetch를 시작
|
||||
|
||||
freshness-sensitive command/read-after-write는 mutation receipt/revision 또는
|
||||
authoritative refetch contract를 사용한다.
|
||||
|
||||
### 10. Conditional revalidation
|
||||
|
||||
REST app-managed ETag profile만 internal validator metadata를 사용할 수 있다.
|
||||
|
||||
```text
|
||||
ValidatorBinding
|
||||
query definition/fingerprint
|
||||
scope fingerprint
|
||||
runtime generation
|
||||
representation version
|
||||
opaque validator
|
||||
```
|
||||
|
||||
- raw validator는 query value/key/diagnostics에 넣지 않음
|
||||
- 304는 exact binding + existing mapped cache value가 있을 때만 fresh transition
|
||||
- 304 freshness transition은 같은 query-entry cache revision에 CAS가 성공할 때만
|
||||
commit하며, concurrent 200/removal 뒤의 late 304를 폐기
|
||||
- value가 없거나 wrong generation이면 304 success로 만들지 않음
|
||||
- validator mismatch/full 200은 normal schema/mapper/admission을 다시 수행하고
|
||||
mapped value commit과 validator install/update를 하나의 entry transaction으로
|
||||
취급
|
||||
- Query removal/GC, scope/logout/account switch, release/contract/schema/mapper
|
||||
epoch 변경과 incompatible cache clear에서 validator sidecar도 함께 폐기
|
||||
- ordinary invalidation 때 validator를 conditional refetch까지 보존할지 즉시
|
||||
폐기할지는 profile이 고정하며 query entry와 독립적으로 남기지 않음
|
||||
|
||||
GraphQL/gRPC metadata를 arbitrary ETag로 해석하지 않는다. application revision
|
||||
field를 schema/mapper가 명시적으로 제공한 경우 별도 revalidation policy가
|
||||
사용한다.
|
||||
|
||||
### 11. Cursor pagination
|
||||
|
||||
```text
|
||||
PaginationProfile
|
||||
CURSOR_SINGLE_PAGE
|
||||
CURSOR_INFINITE
|
||||
OFFSET_STABLE
|
||||
```
|
||||
|
||||
cursor page:
|
||||
|
||||
```text
|
||||
CursorPage<T>
|
||||
items
|
||||
nextCursor | null
|
||||
hasMore
|
||||
snapshotToken | null
|
||||
```
|
||||
|
||||
page invariant:
|
||||
|
||||
- `hasMore === (nextCursor !== null)`을 codec에서 강제한다.
|
||||
- page item count는 requested/implementation ceiling 이하다.
|
||||
- chain 안의 snapshot token은 provider profile이 허용한 null/동일 값만 사용한다.
|
||||
- `hasMore=true`인 empty/non-progress page는 explicit sparse-page profile이 없으면
|
||||
contract failure다.
|
||||
|
||||
`CURSOR_INFINITE` root key:
|
||||
|
||||
- filters/sort/page size semantics
|
||||
- scope
|
||||
- page definition version
|
||||
- cursor 제외
|
||||
|
||||
`CURSOR_INFINITE` page parameter:
|
||||
|
||||
- adapter-private/opaque bounded cursor
|
||||
- previous page/snapshot binding
|
||||
- raw cursor를 diagnostics/URL state/persistence에 임의 저장하지 않음
|
||||
|
||||
`CURSOR_SINGLE_PAGE`:
|
||||
|
||||
- first page의 null marker 또는 current cursor의 runtime-scoped non-reversible
|
||||
identity token을 `canonicalSemanticInput`에 포함한다.
|
||||
- raw cursor는 bound executor closure에만 두며 query key/value/diagnostics에 넣지
|
||||
않는다.
|
||||
- runtime-private exact equality guard/token binding이 실패하면 single-page cache
|
||||
admission을 끄고 closed failure로 끝낸다.
|
||||
- runtime-scoped token을 쓰는 `CURSOR_SINGLE_PAGE`는 `MEMORY_ONLY`다.
|
||||
|
||||
infinite policy:
|
||||
|
||||
- max pages
|
||||
- max total items
|
||||
- max estimated bytes
|
||||
- repeated cursor/non-progress/loop detection
|
||||
- page eviction direction
|
||||
- refresh strategy: first page only, visible window 또는 complete bounded chain
|
||||
- item stable identity/duplicate/revision conflict policy
|
||||
- snapshot changed 시 old/new page를 섞지 않고 restart
|
||||
|
||||
pagination persistence는 기본 disabled다. `CURSOR_SINGLE_PAGE`를 durable하게
|
||||
만들려면 stable partition-bound keyed codec/key lifecycle을 별도 ADR로 승인해야
|
||||
하며 runtime token을 persistence key로 재사용하지 않는다. `CURSOR_INFINITE`
|
||||
persistence를 선택하려면 VD-13 profile이 cursor와 snapshot의
|
||||
classification/expiry, maximum persisted pages/bytes, restored `pageParams` 사용
|
||||
여부를 명시적으로 승인해야 한다. raw sensitive cursor 또는 만료 후 page
|
||||
parameter를 IndexedDB에 저장하지 않는다.
|
||||
|
||||
offset pagination은 insert/delete drift를 허용하는 dataset에서 사용하지 않는다.
|
||||
|
||||
### 12. Mutation Definition
|
||||
|
||||
```text
|
||||
MutationDefinition<Input, Value>
|
||||
definitionId
|
||||
operationId
|
||||
inputCodec
|
||||
logicalKeyCodec
|
||||
commandIdentityTokenCodecId
|
||||
concurrencyPolicy
|
||||
duplicatePolicy
|
||||
optimisticPolicyId | null
|
||||
invalidationTopicRefs[] = { topicId, topicVersion }
|
||||
seedPolicyId | null
|
||||
conflictPolicyId
|
||||
effectCertaintyPolicy
|
||||
owner
|
||||
```
|
||||
|
||||
presentation:
|
||||
|
||||
```text
|
||||
useApplicationMutation(boundMutation)
|
||||
```
|
||||
|
||||
caller는 raw optimistic query key/update function과 invalidation topic을 조립하지
|
||||
않는다.
|
||||
|
||||
mutation topic ref는 unique 0..16개이고 모두 VD-13 global topic registry의 exact
|
||||
version으로 resolve돼야 한다. 한 ref가 가리키는 bounded namespace set만
|
||||
invalidate하며 caller가 runtime에 topic을 추가하지 못한다.
|
||||
|
||||
### 13. Mutation concurrency
|
||||
|
||||
```text
|
||||
ConcurrencyPolicy
|
||||
PARALLEL
|
||||
SERIAL_BY_LOGICAL_KEY
|
||||
SUPERSEDE_PENDING_READ_BY_LOGICAL_KEY
|
||||
REJECT_WHILE_ACTIVE_BY_LOGICAL_KEY
|
||||
|
||||
DuplicatePolicy
|
||||
JOIN_IDENTICAL
|
||||
REJECT_DUPLICATE
|
||||
ALLOW_INDEPENDENT
|
||||
```
|
||||
|
||||
- logical key는 validated input의 approved opaque identity
|
||||
- command identity token은 operation/definition version, current scope
|
||||
partition과 **전체 validated semantic input**을
|
||||
`RuntimeIdentityTokenCodecV1`으로 intern해 만든다. UI transient field와
|
||||
transport bytes/idempotency key는 canonical equality source에 포함하지 않는다.
|
||||
- logical key는 serialization/conflict group이고 exact canonical equality +
|
||||
identity token은 동일 command 판별 값이다. 두 값을 서로 대체하지 않는다.
|
||||
- `JOIN_IDENTICAL`은 exact equality guard도 통과한 같은 identity token의 기존
|
||||
in-flight Promise와 terminal result를 공유하며 transport/optimistic layer를
|
||||
추가하지 않는다.
|
||||
- `REJECT_DUPLICATE`는 exact-identical token이 active이면 fetch 0회와 closed
|
||||
`DUPLICATE_IN_FLIGHT`를 반환한다.
|
||||
- `ALLOW_INDEPENDENT`는 같은 exact identity도 독립 command로 실행한다. backend
|
||||
replay/idempotency와 UX가 이를 명시적으로 허용한 operation에만 등록한다.
|
||||
- distinct input을 같은 Promise에 join하지 않음
|
||||
- same hook, two hooks, two routes의 coordinator가 동일 policy를 사용
|
||||
- coordinator는 hook-local singleton이 아니라 runtime/scope 수명의 registry-owned
|
||||
service이며 scope generation 전환에서 신규 admission을 닫고 late commit을 fence
|
||||
- `SUPERSEDE`는 이미 server로 보낸 non-replayable command를 cancel/rollback했다고
|
||||
가정하지 않음
|
||||
- local serialization은 server idempotency/concurrency authority가 아님
|
||||
- scope/generation change는 pending result commit을 fence
|
||||
|
||||
### 14. Optimistic patch
|
||||
|
||||
snapshot 전체 restore만 사용하지 않는다.
|
||||
|
||||
```text
|
||||
OptimisticLayer
|
||||
mutationId
|
||||
logicalKey
|
||||
commandIdentityToken
|
||||
baseCacheRevision
|
||||
expectedEntityRevision | null
|
||||
patch
|
||||
inversePatch
|
||||
affectedQueryDefinitions
|
||||
```
|
||||
|
||||
선택 가능한 구현:
|
||||
|
||||
- cache entry revision CAS
|
||||
- ordered optimistic layer log
|
||||
- operation-specific compare-and-apply patch
|
||||
|
||||
공통:
|
||||
|
||||
1. affected exact queries cancel
|
||||
2. bounded current revision/value 확인
|
||||
3. registered pure patch 적용
|
||||
4. other mutation layer와 ordering 보존
|
||||
5. failure에서 자기 layer만 제거/역적용
|
||||
6. success result/revision과 reconcile
|
||||
7. invalidation/refetch
|
||||
|
||||
old whole snapshot을 복원해 다른 mutation commit을 덮지 않는다.
|
||||
|
||||
optimistic update를 하지 않는 조건:
|
||||
|
||||
- snapshot/patch/result byte ceiling 초과
|
||||
- cache entry missing/wrong revision
|
||||
- non-deterministic merge
|
||||
- high-conflict command
|
||||
- scope/generation transition
|
||||
- unknown effect certainty
|
||||
|
||||
그 경우 pending UX만 보여 주고 server response/refetch를 기다린다.
|
||||
|
||||
### 15. Effect certainty와 conflict
|
||||
|
||||
```text
|
||||
MutationEffect
|
||||
NOT_APPLIED
|
||||
COMMITTED
|
||||
UNKNOWN
|
||||
```
|
||||
|
||||
- timeout/cancel/network failure가 `NOT_APPLIED`를 자동 의미하지 않음
|
||||
- keyed backend status/receipt가 있어야 ambiguous command reconcile 가능
|
||||
- `UNKNOWN`은 새 idempotency key로 자동 retry 금지
|
||||
- 409 business conflict, REST 412, GraphQL safe conflict code, gRPC `ABORTED`를
|
||||
common conflict surface로 mapping하되 의미 차이는 feature policy가 소유
|
||||
- server revision/merge/overwrite decision은 cache가 아니라 use case 소유
|
||||
|
||||
### 16. Mutation success, seed와 invalidation
|
||||
|
||||
server commit 뒤:
|
||||
|
||||
- exact returned result를 schema/mapper/fence/budget 검증
|
||||
- registered detail seed policy가 있으면 exact current entity/revision만 write
|
||||
- list/aggregate는 default invalidate
|
||||
- list patch는 deterministic sort/filter/membership policy가 있을 때만
|
||||
- invalidation topic은 query key가 아닌 opaque registry identity
|
||||
- current mutation lease 동안 remote hints coalesce
|
||||
|
||||
local invalidation failure는 committed command를 failure로 바꾸지 않는다.
|
||||
cache health를 degraded로 기록하고 bounded authoritative refetch를 예약한다.
|
||||
|
||||
### 17. Cross-context
|
||||
|
||||
현재 cross-tab wire는 invalidate-only다. 유지한다.
|
||||
|
||||
- query data/key/input/cursor/validator를 broadcast하지 않음
|
||||
- remote event는 authority가 아니라 stale hint
|
||||
- account/scope/version 검증은 VD-13
|
||||
- mutation ordering/optimistic layer를 tab 간 복제하지 않음
|
||||
- sequence gap은 모든 registered namespace를 stale 처리하되 active query만
|
||||
bounded refetch한다. inactive query는 다음 mount/focus에서 revalidate하고,
|
||||
persistence가 선택된 경우 durable ledger refresh는 VD-13 절차를 따른다.
|
||||
|
||||
### 18. GraphQL과 normalized cache
|
||||
|
||||
persisted GraphQL query도 mapped operation result를 TanStack에 cache한다.
|
||||
|
||||
- query key는 semantic application input
|
||||
- GraphQL document/hash는 key/value에 없음
|
||||
- GraphQL SDK cache는 `no-cache`/disabled
|
||||
- partial data default reject
|
||||
- approved partial result는 completeness metadata를 application contract가 소유
|
||||
- missing/error field를 previous complete value와 자동 merge하지 않음
|
||||
|
||||
normalized entity cache가 필요하면:
|
||||
|
||||
- bounded context 하나가 TanStack operation cache를 대체
|
||||
- key fields/typename, eviction, pagination merge, optimistic layer, logout/scope,
|
||||
persistence와 removal을 별도 ADR
|
||||
- dual write/read 금지
|
||||
|
||||
현재 `NOT_SELECTED`다.
|
||||
|
||||
### 19. Connect/gRPC-Web server stream
|
||||
|
||||
ordinary Query는 terminal operation 결과를 전제로 한다.
|
||||
|
||||
- unary는 normal query 가능
|
||||
- finite server stream을 complete aggregate로 쓸 경우 staging buffer에 bounded
|
||||
accumulate하고 valid Connect EndStream 또는 gRPC-Web terminal status,
|
||||
schema/mapper/fence 뒤 atomic cache commit
|
||||
- long-running stream은 `ServerStreamPort`와 registered reducer/invalidation owner
|
||||
- frame마다 query cache를 append하여 unbounded event history를 만들지 않음
|
||||
- stream reconnect를 Query retry로 하지 않음
|
||||
- gap/overflow는 current snapshot 폐기 또는 authoritative query refetch
|
||||
|
||||
### 20. Persistence, SSR와 offline
|
||||
|
||||
- memory cache runtime은 `COMPOSED`
|
||||
- IndexedDB query persister reference는 VD-13 기준
|
||||
`DESIGNED_NOT_IMPLEMENTED`
|
||||
- product persistence는 `NOT_SELECTED`
|
||||
- SSR hydration은 `NOT_SELECTED`
|
||||
- offline mutation queue는 `NOT_SELECTED`
|
||||
|
||||
VD-25 profile은 persistence를 직접 켜지 않는다. joined VD-13
|
||||
scope/persistence profile, reference runtime과 제품 allowlist/retention/scope가
|
||||
모두 구현·선택된 query만 IndexedDB persistence를 사용할 수 있다.
|
||||
|
||||
server-state policy를 이유로 Web Storage에 query payload를 넣지 않는다.
|
||||
|
||||
### 21. Security와 privacy
|
||||
|
||||
- authorization result를 staleTime/cache hit으로 대체하지 않음
|
||||
- account/logout transition에서 sensitive data를 즉시 fence/hide
|
||||
- query key에 raw PII/account/resource ID/URL/document/message 금지
|
||||
- cache value에 credential/header/validator/trace/error raw data 금지
|
||||
- optimistic layer에도 command token/raw body를 저장하지 않음
|
||||
- command identity token/logical key를 diagnostics, cross-context wire나 persistence에
|
||||
넣지 않음
|
||||
- developer tools/diagnostics production exposure policy
|
||||
- cache poisoning 방지를 위해 schema/mapper/result contract와 generation 검증
|
||||
- cross-context event에 data 없음
|
||||
|
||||
### 22. Observability
|
||||
|
||||
허용:
|
||||
|
||||
- query/mutation definition/profile ID
|
||||
- hit/miss/stale/fresh/refresh/evict outcome
|
||||
- result item/estimated-byte/page bucket
|
||||
- runtime identity intern entry/canonical-byte/active-lease bucket
|
||||
- focus/reconnect/invalidation refetch reason
|
||||
- mutation concurrency/duplicate/optimistic/rollback/conflict/effect bucket
|
||||
- invalidation/seed/degraded recovery outcome
|
||||
- scope/profile version의 low-cardinality bucket
|
||||
|
||||
금지:
|
||||
|
||||
- query key/input/value
|
||||
- command identity token/logical key
|
||||
- identity intern canonical bytes/token actual value
|
||||
- cursor/snapshot/validator/revision actual value
|
||||
- resource/account/tenant ID
|
||||
- GraphQL/protobuf/REST DTO
|
||||
- optimistic patch/snapshot
|
||||
|
||||
### 23. Testing
|
||||
|
||||
query definition/key:
|
||||
|
||||
- definition/input/key/executor type/runtime binding
|
||||
- VD-13↔VD-25 topic set/version exact join과 bounded many-to-many fan-out
|
||||
- runtime identity token same-input stability, random-token collision regeneration과
|
||||
bounded failure의 cache/join 0회
|
||||
- intern entry/total-byte ceiling, active non-eviction, Query GC/mutation terminal
|
||||
lease release와 runtime-close leak 0
|
||||
- undefined/NaN/Date/class/accessor/cycle/sparse/oversize collision fixture
|
||||
- same semantic input stable identity
|
||||
- protocol wire identity 변화가 key에 들어가지 않음
|
||||
- per-profile stale/gc/refetch/retry owner
|
||||
|
||||
cache admission:
|
||||
|
||||
- mapped immutable plain value only
|
||||
- item/estimated byte cap
|
||||
- schema/mapper/generation failure write 0
|
||||
- auth/contract failure stale visibility
|
||||
|
||||
mutation:
|
||||
|
||||
- same semantic command identity, same logical key의 distinct input
|
||||
- same hook/two hooks/two routes
|
||||
- runtime/scope coordinator의 logical-key serial/parallel과
|
||||
join/reject/independent duplicate 결과
|
||||
- out-of-order success/failure
|
||||
- optimistic layer/CAS rollback without overwriting other commit
|
||||
- effect `NOT_APPLIED/COMMITTED/UNKNOWN`
|
||||
- server response detail seed + list invalidate
|
||||
- invalidation failure after commit
|
||||
- account switch during pending command
|
||||
|
||||
pagination:
|
||||
|
||||
- null/repeated/cyclic cursor
|
||||
- single-page cursor identity-token collision/isolation과 memory-only enforcement
|
||||
- `hasMore`/`nextCursor` 불일치와 snapshot drift
|
||||
- snapshot change
|
||||
- max page/item/byte
|
||||
- page eviction/refetch
|
||||
- duplicate identity/revision policy
|
||||
- cancellation and late page
|
||||
|
||||
integration:
|
||||
|
||||
- REST/GraphQL/gRPC unary → schema → mapper → cache → UI
|
||||
- focus/reconnect/offline/stale-degraded
|
||||
- conditional 304 exact binding
|
||||
- cross-tab invalidation/mutation lease
|
||||
- logout/account/release generation
|
||||
- finite stream atomic commit and overflow
|
||||
|
||||
### 24. Rollout
|
||||
|
||||
1. VD-13 strict key codec/scope snapshot interface를 확정한다.
|
||||
2. current `useApplicationQuery({queryKey, execute})` 뒤에 bound definition adapter를
|
||||
추가한다.
|
||||
3. reference queries를 shadow key/policy로 비교하되 secondary cache write 금지.
|
||||
4. per-profile policy/result ceiling을 read query에 canary한다.
|
||||
5. typed mutation definition과 input-aware coordinator를 도입한다.
|
||||
6. optimistic layer/CAS를 low-conflict command에만 canary한다.
|
||||
7. cursor page reference vertical을 구현한다.
|
||||
8. arbitrary key/executor와 raw optimistic callback API를 제거한다.
|
||||
9. account/generation browser evidence와 runbook drill 뒤 traffic을 올린다.
|
||||
|
||||
rollback:
|
||||
|
||||
- 신규 query/mutation admission/optimistic patch를 kill switch로 닫음
|
||||
- current scope queries cancel
|
||||
- unsafe/incompatible memory cache clear
|
||||
- pending command effect certainty reconcile
|
||||
- current basic facade 또는 no-optimistic authoritative refetch로 downgrade
|
||||
- schema/mapper/query definition/backend artifact를 coherent set으로 복구
|
||||
|
||||
## 규범 기준
|
||||
|
||||
- [TanStack Query v5 Important Defaults](https://tanstack.com/query/v5/docs/framework/react/guides/important-defaults)
|
||||
- [TanStack Query v5 Query Cancellation](https://tanstack.com/query/v5/docs/framework/react/guides/query-cancellation)
|
||||
|
||||
## 완료 기준
|
||||
|
||||
- caller가 arbitrary key/executor/TanStack option을 조합할 수 없다.
|
||||
- strict key codec과 VD-13 scope projection이 모든 query에 적용된다.
|
||||
- VD-13 normative key layout과 scope/persistence profile을 재정의하지 않고 exact
|
||||
join한다.
|
||||
- mapped/bounded/current-generation value만 cache에 들어간다.
|
||||
- transport와 Query retry owner가 중복되지 않는다.
|
||||
- cursor page가 next/snapshot/loop/page/item/byte ceiling을 갖는다.
|
||||
- runtime-private exact equality guard까지 통과한 command identity만 declared
|
||||
join되고 distinct input이 같은 Promise로 잘못 join되지 않는다.
|
||||
- concurrent optimistic rollback이 다른 committed update를 덮지 않는다.
|
||||
- effect certainty, conflict, seed와 invalidation owner가 operation별로 닫힌다.
|
||||
- GraphQL normalized dual cache와 unbounded stream cache가 없다.
|
||||
- scope/logout/provider fault와 rollback/removal evidence가 통과한다.
|
||||
Reference in New Issue
Block a user