803 lines
29 KiB
Markdown
803 lines
29 KiB
Markdown
# 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가 통과한다.
|