1681 lines
71 KiB
Markdown
1681 lines
71 KiB
Markdown
# Client cache and browser storage platform
|
|
|
|
> **정본 안내 (non-authoritative for runtime capability decisions)**
|
|
>
|
|
> Runtime Config/boot, Fetch HTTP client, Router, Query/Mutation, realtime 공통 경계, Web Worker,
|
|
> Service Worker, offline command와 Background Sync의 구현 결정은
|
|
> [프론트엔드 런타임 Capability 저장소 정합형 구현 결정 폐쇄 상세 설계](./2026-07-30-frontend-runtime-capability-repository-aligned-implementation-closed-deep-design.md)가 정본이다.
|
|
> 본 문서의 해당 서술이 정본과 충돌하면 정본을 따른다.
|
|
|
|
|
|
- 상태: capability별 current/target 상태 분리
|
|
- 기준일: 2026-07-28
|
|
- 범위: TanStack Query memory cache, Local/Session Storage, IndexedDB query
|
|
persistence 경계, 탭 간 invalidation
|
|
- 관련 결정:
|
|
[VD-13 client cache scope와 persistence](./decisions/VD-13-client-cache-scope-and-persistence.md)
|
|
- query persistence reference 상태: `DESIGNED_NOT_IMPLEMENTED`
|
|
- query persistence product 상태: `NOT_SELECTED`
|
|
|
|
이 문서는 server state cache와 browser persistence를 프로덕션에서 운영할 때의
|
|
소유권, 정책, protocol, 실패·복구 및 promotion evidence를 정의한다. 네 기술은
|
|
모두 값을 잠시 보관할 수 있지만 같은 저장소가 아니다.
|
|
|
|
| 기술 | 기본 책임 | 기본 수명 | 현재 skeleton 상태 |
|
|
| --- | --- | --- | --- |
|
|
| TanStack Query memory cache | 현재 browser context의 server state 표시와 refetch 조정 | page runtime | `COMPOSED` |
|
|
| `localStorage` | 작은 공개 preference와 제한된 opaque control record | browser session을 넘어 지속 | `COMPOSED`, 등록 key만 사용 |
|
|
| `sessionStorage` | 현재 top-level tab의 작은 reload/session guard | tab page session | `COMPOSED`, 등록 key만 사용 |
|
|
| IndexedDB | 큰 구조화 record와 transaction | 명시적 retention까지 | native reference runtime `AVAILABLE_NOT_COMPOSED` |
|
|
| cross-tab invalidation | 다른 context에 stale 가능성을 알리는 best-effort hint | event | `COMPOSED`, unavailable이면 local-only |
|
|
| IndexedDB query persistence | 승인된 query projection의 optional warm restore | 승인된 max age까지 | `DESIGNED_NOT_IMPLEMENTED`, product `NOT_SELECTED` |
|
|
| SSR dehydration/hydration | request-scoped server cache를 browser로 전달 | 한 SSR navigation | `NOT_SELECTED` |
|
|
|
|
이 문서와 VD-13은 다음 표준 상태만 사용한다.
|
|
|
|
| 상태 | 의미 |
|
|
| --- | --- |
|
|
| `COMPOSED` | 구현과 test가 있고 production bootstrap이 실제 생성·소비한다. |
|
|
| `AVAILABLE_NOT_COMPOSED` | reusable runtime과 test가 있지만 production bootstrap에서 생성하지 않는다. |
|
|
| `DESIGNED_NOT_IMPLEMENTED` | 경계와 invariant만 승인됐고 실행 코드는 없다. |
|
|
| `NOT_SELECTED` | 제품 요구·owner·policy가 승인되지 않았다. |
|
|
| `PLATFORM_LIMITED` | browser/platform이 요구 의미를 cross-browser로 보장하지 못한다. |
|
|
|
|
현재 `src/adapters/query-cache/tanstack-query-cache.ts`는 한 runtime당
|
|
`QueryClient`를 만들고, query retry를 끄며, 30초 `staleTime`과 5분 `gcTime`을
|
|
기본값으로 사용한다. `src/bootstrap/runtime-adapters.ts`와
|
|
`src/bootstrap/runtime-application.tsx`가 이 client를 실제 provider tree에
|
|
조립한다.
|
|
|
|
현재 `src/contracts/storage-keys.ts`에는 `COLOR_SCHEME`,
|
|
`CHUNK_RELOAD_GUARD`, 금지된 `AUTH_TOKEN`과 비활성
|
|
`QUERY_PERSISTENCE`가 등록되어 있다.
|
|
`src/adapters/storage/browser-storage-codec.ts`와
|
|
`browser-storage-adapter.ts`는 closed JSON codec, 기본 16,384-byte 상한, schema
|
|
envelope, TTL과 동일 envelope를 쓰는 memory fallback을 제공한다.
|
|
`StorageDefinition.valueCodec`은 `color-scheme-v1`, `opaque-string-v1`, `none`의
|
|
closed registry이며 adapter가 write와 read 양쪽에서 key별 codec을 적용한다.
|
|
다만 byte 상한은 아직 adapter instance의 공통 값이고 key별 byte policy,
|
|
`HIT`/`MISS`, durability-aware success, opaque partition binding, logout action과
|
|
lifecycle sweep는 없다. memory fallback은 실제 current-runtime value를
|
|
보관하면서도 write 결과를 degraded success가 아닌 `{ok: false}`로 반환한다.
|
|
registry의 `migration`은 `discard`만 허용하며 실제 adjacent migration read path는
|
|
없다. expiry가 registry max TTL 안에서 발급됐는지 확인하는 clock-skew/future
|
|
검증과 quota cleanup 뒤 exact one-time retry도 아직 없다.
|
|
|
|
현재 `src/contracts/cache-invalidation.ts`,
|
|
`src/adapters/cross-context-invalidation/`,
|
|
`src/adapters/query-cache/tanstack-cache-coordinator.ts`와
|
|
`src/presentation/adapters/query/query-invalidation-provider.tsx`에는
|
|
2,048-byte closed event, `BroadcastChannel` 우선·`localStorage` pulse fallback,
|
|
self/duplicate/sequence-gap 처리와 registry topic을 TanStack namespace로
|
|
투영하는 runtime이 있다. `src/bootstrap/runtime-adapters.ts`가 release ID에서
|
|
`cacheEpoch`를 만들고 transport/coordinator를 조립하며,
|
|
`runtime-application.tsx`가 `QueryInvalidationProvider`를 production provider
|
|
tree에 연결한다. `BroadcastChannel`과 storage pulse를 모두 쓸 수 없으면 boot를
|
|
실패시키지 않고 `DEGRADED_LOCAL_ONLY`로 동작한다.
|
|
|
|
native two-page browser spec은 BroadcastChannel primary와 localStorage fallback
|
|
delivery/cleanup을 검증한다. 다만 production QueryClient/coordinator까지 연결한
|
|
account transition, bfcache/StrictMode lifecycle E2E는 없고, 현재 보존 artifact도
|
|
Chromium/Firefox만 통과해 WebKit을 포함한 promotion evidence는 충족하지 않는다.
|
|
localStorage fallback physical key는 아직 storage registry 밖의 상수이고 native
|
|
`StorageEvent.storageArea`도 facade에서 검증하지 않는다.
|
|
|
|
현재 epoch 구현은 `release.<releaseId>` 하나다. session/account epoch, opaque
|
|
account partition, scope 전환 시 새 QueryClient 생성과 durable namespace epoch는
|
|
아직 구현되지 않았다. 따라서 현재 cross-tab event는 같은 release의 등록 query
|
|
topic을 빠르게 stale 처리하는 용도에 한정된다.
|
|
|
|
`QUERY_REGISTRY`의 모든 현재 entry와
|
|
`createTanStackCacheCoordinator()`는 `persistence: "disabled"`만 허용한다.
|
|
`QUERY_PERSISTENCE` Web Storage key도 `disabled`/`sensitive-forbidden`이다.
|
|
IndexedDB query persister, hydration과 durable invalidation ledger는 구현·조립하지
|
|
않았다.
|
|
|
|
현재 query key helper의 `canonicalize()`는 plain object 여부나 depth/byte/node
|
|
상한을 검증하지 않고 cycle, accessor, `BigInt`, function/symbol, non-finite
|
|
number와 native object를 fail-closed하지 않는다. installed registry도
|
|
classification, scope, per-query stale/gc/result budget을 실행 정책으로 강제하지
|
|
않고 global QueryClient default와 namespace/invalidation mapping만 사용한다.
|
|
|
|
IndexedDB는 이미
|
|
`src/application/ports/browser-file-storage/indexeddb-port.ts`와
|
|
`src/adapters/storage/indexeddb/`에 transaction-complete, schema/codec
|
|
migration, revision CAS, idempotency, opaque dataset binding, retention, quota와
|
|
blocked/versionchange를 다루는 reference runtime이 있다. 이 설계는 그 runtime을
|
|
`StoragePort` backend enum으로 다시 구현하지 않는다. TanStack Query persistence를
|
|
선택하는 경우에만 별도의 query-cache codec/facade를 그 runtime 위에 조합한다.
|
|
|
|
## 0. current delta와 implementation authority
|
|
|
|
다음 표는 **현재 실행 코드**와 VD-13의 **목표 계약**을 구분한다.
|
|
|
|
| capability | primary current status | current implementation | target delta | delta implementation status |
|
|
| --- | --- | --- | --- | --- |
|
|
| memory QueryClient | `COMPOSED` | runtime별 생성, global stale/gc/retry default | session/account scope owner, generation fence, per-query policy | `DESIGNED_NOT_IMPLEMENTED` |
|
|
| application query bridge | `COMPOSED` | AbortSignal, stale error, optimistic rollback/conflict surface | old-generation query/mutation callback 폐기 | `DESIGNED_NOT_IMPLEMENTED` |
|
|
| Web Storage | `COMPOSED` | 두 key, strict v1 envelope/codec, lazy TTL, global 16 KiB cap | per-key cap, HIT/MISS, durability, v2 scope, migration, sweep/logout | `DESIGNED_NOT_IMPLEMENTED` |
|
|
| cross-tab invalidation | `COMPOSED` | release epoch, versioned hint, duplicate/gap, BC→localStorage | composite scope epoch, registered pulse, storageArea, durable ledger hook | `DESIGNED_NOT_IMPLEMENTED` |
|
|
| generic IndexedDB | `AVAILABLE_NOT_COMPOSED` | reusable repository/maintenance runtime | product dataset schema/codec/query/policy composition | product selection `NOT_SELECTED` |
|
|
| query persistence facade | `DESIGNED_NOT_IMPLEMENTED` | 없음, registry가 disabled 강제 | stable per-query record, bounded restore, namespace epoch/CAS | `DESIGNED_NOT_IMPLEMENTED` |
|
|
| product persistence | `NOT_SELECTED` | owner/query allowlist 없음 | measured need와 explicit composition | `NOT_SELECTED` |
|
|
| SSR | `NOT_SELECTED` | browser SPA composition | request-scoped client, safe dehydrate/merge precedence | `NOT_SELECTED` |
|
|
| exactly-once tab delivery | `PLATFORM_LIMITED` | browser acknowledgement protocol 없음 | cross-browser exactly-once target 없음 | `PLATFORM_LIMITED` |
|
|
|
|
scope authority, query/data classification, product persistence allowlist,
|
|
retention, conflict UX와 offline sync는 composition/product owner가 결정한다.
|
|
closed codec, absolute ceiling, lifecycle state machine, transaction ordering,
|
|
wrong-scope rejection, listener cleanup과 safe failure mapping은 공통 runtime이
|
|
구현해야 한다. backend는 server revision/ETag, mutation idempotency와 실제
|
|
offline sync를 선택한 경우의 cursor/resume/conflict protocol을 소유한다.
|
|
|
|
이 문서의 최상위 불변조건은 다음과 같다.
|
|
|
|
> memory cache hit, browser persistence restore, 탭 간 invalidation 수신과 서버의
|
|
> 최신 상태는 서로 다른 사실이다. 어느 하나도 authorization, 최신성 또는 다른
|
|
> 단계의 성공을 암묵적으로 보장하지 않는다.
|
|
|
|
## 1. 변경할 수 없는 설계 결정
|
|
|
|
### 1.1 하나의 범용 cache/storage port로 합치지 않는다
|
|
|
|
```text
|
|
server response
|
|
-> application result
|
|
-> React query bridge
|
|
-> TanStack Query memory cache
|
|
|
|
small approved preference/control value
|
|
-> application preference port
|
|
-> registered Web Storage adapter
|
|
|
|
optional reconstructable query projection
|
|
-> query persistence facade
|
|
-> query-specific codec
|
|
-> IndexedDB reference runtime
|
|
|
|
mutation commit or scope reset
|
|
-> local cache action
|
|
-> optional durable cache epoch commit
|
|
-> cross-tab invalidation hint
|
|
```
|
|
|
|
`CachePort`, `StoragePort`, `BrowserStore` 하나에 memory, Web Storage와 IndexedDB를
|
|
backend option으로 넣으면 다음 차이가 사라진다.
|
|
|
|
- TanStack의 freshness와 observer lifecycle
|
|
- Web Storage의 동기식 main-thread 비용
|
|
- IndexedDB의 asynchronous transaction commit
|
|
- `localStorage`와 `sessionStorage`의 context 범위
|
|
- cache invalidation hint의 전달 손실 가능성
|
|
- logout에서 invalidate가 아니라 remove/reset이 필요한 이유
|
|
|
|
### 1.2 source of truth
|
|
|
|
- 일반적인 server state의 source of truth는 서버다.
|
|
- TanStack memory cache와 persisted query projection은 재구성 가능한 복사본이다.
|
|
- cache entry의 존재는 authorization proof가 아니다. 모든 network request는
|
|
현재 session credential과 서버 authorization을 다시 통과한다.
|
|
- `staleTime`은 freshness optimization이다. 업무상 유효 기간이나 권한 수명이
|
|
아니다.
|
|
- `gcTime`은 inactive query의 memory retention이다. active query 삭제, logout
|
|
purge 또는 persisted TTL이 아니다.
|
|
- cross-tab event는 다른 tab에 revalidation 필요성을 알리는 hint다. delivery,
|
|
ordering, exactly-once 또는 server commit을 증명하지 않는다.
|
|
- business offline record, unsynced command와 local-first draft는 query persistence가
|
|
아니다. 실제 요구가 생기면 feature-specific application repository와 use case를
|
|
별도로 만든다.
|
|
|
|
### 1.3 domain, application과 infrastructure 경계
|
|
|
|
TanStack Query는 presentation infrastructure다.
|
|
|
|
- `QueryClient`, `Query`, `DehydratedState`, persister type은
|
|
application/domain에 노출하지 않는다.
|
|
- feature query hook은 application input을 호출하고 `AbortSignal`, query key,
|
|
stale/refresh 상태, 성공 후 invalidation을 연결하는 inbound adapter다.
|
|
- page는 raw `QueryClient`나 native storage를 직접 사용하지 않는다.
|
|
- 기존 `QueryCachePort`는 application use case가 cache 일관성을 업무 규칙으로
|
|
실제 요구할 때만 의미가 있다. 일반적인 TanStack 기능 전체를
|
|
`read/write/invalidate`로 다시 추상화하지 않는다.
|
|
|
|
Web Storage는 작은 preference용 outbound adapter가 될 수 있다. 어떤 preference를
|
|
보관하는지가 application 계약에 포함되더라도 `Storage`, physical key, JSON,
|
|
schema envelope와 quota exception은 adapter 밖으로 나오지 않는다.
|
|
|
|
IndexedDB query persistence와 cross-tab invalidation의 직접 소비자는 bootstrap의
|
|
query runtime이다. 제품 domain/use case가 이를 호출하지 않는다. 제품이
|
|
offline-first workflow를 선택한 경우에만 별도의 feature application port를 만들고,
|
|
query cache와 동일 repository를 공유하지 않는다.
|
|
|
|
### 1.4 공통 mechanism과 제품 정책의 분리
|
|
|
|
공통 infrastructure가 소유한다.
|
|
|
|
- key와 event의 syntax validation
|
|
- codec 실행과 closed failure mapping
|
|
- TTL 확인, expired-as-miss와 bounded cleanup
|
|
- byte/count hard cap
|
|
- IndexedDB transaction-complete 판정
|
|
- scope/epoch binding 검증
|
|
- duplicate, self-echo와 unknown protocol event drop
|
|
- listener, timer, channel, DB connection cleanup
|
|
- native exception redaction
|
|
|
|
composition 또는 dataset/query owner가 결정한다.
|
|
|
|
- query namespace와 query key shape
|
|
- data classification
|
|
- stale/gc/max-age
|
|
- persistence 허용 여부
|
|
- account/tenant partition
|
|
- logout/account deletion 동작
|
|
- quota 우선순위와 fallback
|
|
- 어떤 mutation이 어떤 namespace를 invalidate/remove하는지
|
|
- codec와 호환 migration
|
|
|
|
실제 business가 없는 skeleton은 공통 mechanism과 빈/safe registry를 제공할 수
|
|
있지만, 임의의 제품 query를 persistence 대상으로 자동 등록하지 않는다.
|
|
|
|
## 2. 상태와 commit point
|
|
|
|
### 2.1 서로 다른 상태 축
|
|
|
|
한 entry의 상태를 `cached: boolean` 하나로 표현하지 않는다.
|
|
|
|
| 축 | 예시 상태 |
|
|
| --- | --- |
|
|
| memory | `ABSENT`, `FRESH`, `STALE`, `FETCHING`, `INACTIVE` |
|
|
| persistence | `DISABLED`, `RESTORING`, `AVAILABLE`, `DEGRADED`, `UNAVAILABLE` |
|
|
| compatibility | `COMPATIBLE`, `EXPIRED`, `BUSTED`, `CORRUPT`, `FUTURE_VERSION` |
|
|
| scope | `ACTIVE`, `FENCED`, `REVOKED` |
|
|
| cross-tab delivery | `LOCAL_APPLIED`, `PUBLISH_ACCEPTED`, `DROPPED` |
|
|
|
|
`PUBLISH_ACCEPTED`는 다른 tab이 처리했다는 뜻이 아니다. IndexedDB request
|
|
`success`는 transaction이 commit됐다는 뜻이 아니다. query data가 memory에
|
|
남아 있다는 사실은 현재 account가 읽어도 된다는 뜻이 아니다.
|
|
|
|
### 2.2 cache lifecycle
|
|
|
|
```text
|
|
query execute
|
|
-> application/server success
|
|
-> memory cache write
|
|
-> FRESH
|
|
-> staleTime elapsed or explicit invalidation
|
|
-> STALE
|
|
-> active observer: bounded background refetch
|
|
-> no observer: INACTIVE
|
|
-> gcTime elapsed
|
|
-> ABSENT
|
|
```
|
|
|
|
Persistence를 선택한 query만 별도 lifecycle을 가진다.
|
|
|
|
```text
|
|
approved memory entry
|
|
-> codec validation
|
|
-> policy/scope/epoch binding
|
|
-> IndexedDB transaction complete
|
|
-> PERSISTED
|
|
|
|
boot
|
|
-> bounded open/restore
|
|
-> binding + schema + buster + TTL + codec validation
|
|
-> hydrate as reconstructable cache
|
|
-> normal freshness/refetch policy
|
|
```
|
|
|
|
### 2.3 mutation 이후의 순서
|
|
|
|
일반 mutation의 권장 순서는 다음과 같다.
|
|
|
|
```text
|
|
server mutation committed
|
|
-> current scope/generation still active인지 확인
|
|
-> local namespace invalidate 또는 remove
|
|
-> persistence를 사용하면 namespace epoch/tombstone transaction commit
|
|
-> cross-tab hint publish
|
|
-> active query background refetch
|
|
```
|
|
|
|
서버가 실패했거나 commit 여부가 불명확한 mutation에서 성공 invalidation event를
|
|
발행하지 않는다. 반대로 서버 commit 뒤 local invalidation이나 hint 발행이
|
|
실패해도 이미 성공한 server mutation을 실패로 되돌리지 않는다. 이 경우 mutation
|
|
결과는 성공이고 cache synchronization은 별도 `DEGRADED` observation이다.
|
|
|
|
공유 IndexedDB record를 변경한 뒤 다른 tab에 알리는 경우에도 **transaction
|
|
complete가 먼저이고 hint가 나중**이다. hint를 먼저 보내면 receiver가 commit 전
|
|
상태를 다시 읽고 최신으로 오인할 수 있다.
|
|
|
|
## 3. 데이터 분류와 저장 허용표
|
|
|
|
### 3.1 분류
|
|
|
|
| 분류 | 예 | memory query | localStorage | sessionStorage | IndexedDB query persistence | invalidation payload |
|
|
| --- | --- | --- | --- | --- | --- | --- |
|
|
| `PUBLIC` | 공개 reference data, UI preference | 허용 | 작은 값 허용 | 작은 값 허용 | 정책 승인 시 허용 | opaque namespace만 |
|
|
| `INTERNAL` | 로그인 후 재구성 가능한 일반 projection | 허용, scope 필수 | 기본 금지 | 기본 금지 | partition/TTL 승인 시 허용 | opaque namespace만 |
|
|
| `PERSONAL` | 사용자별 response projection | 허용, scope/reset 필수 | 금지 | 기본 금지 | 명시적 보안·retention 승인 시만 | 데이터/ID 금지 |
|
|
| `CONFIDENTIAL` | 높은 민감도의 업무 데이터 | 필요한 순간의 memory만 | 금지 | 금지 | 기본 금지 | 금지 |
|
|
| `CREDENTIAL` | token, password, signing key, raw authorization | 금지 | 금지 | 금지 | 금지 | 금지 |
|
|
|
|
short-lived signed URL/capability, cookie, access/refresh token, password,
|
|
authorization header, raw session object, cryptographic key, `File`, `Blob`,
|
|
object URL과 native handle을 persistence 또는 invalidation payload에 넣지 않는다.
|
|
|
|
### 3.2 client-side encryption의 한계
|
|
|
|
같은 origin의 JavaScript가 ciphertext와 key를 모두 읽을 수 있으면 client-side
|
|
encryption은 XSS 또는 악성 same-origin script에 대한 authorization boundary가
|
|
아니다. Web Crypto는 승인된 외부 key lifecycle이 있을 때 disk/backup 노출을
|
|
줄이는 defense-in-depth가 될 수 있지만, 금지된 data class를 허용하는 근거가
|
|
아니다.
|
|
|
|
### 3.3 authority와 eviction
|
|
|
|
query persistence 대상은 항상 `SERVER` authority이면서 `RECONSTRUCTABLE`이어야
|
|
한다. user-authored unsynced value를 query cache quota cleanup으로 삭제하면 안
|
|
된다. local-first 또는 `UNTIL_SYNCED` dataset은 기존 IndexedDB feature repository
|
|
정책을 사용하고 query persistence store와 분리한다.
|
|
|
|
## 4. scope, registry와 policy snapshot
|
|
|
|
### 4.1 opaque cache scope
|
|
|
|
모든 account/tenant 종속 cache는 다음과 같은 registry-issued scope에 묶는다.
|
|
|
|
```ts
|
|
type CacheScopeSnapshot = Readonly<{
|
|
protocolVersion: 1;
|
|
authorityToken: string;
|
|
partitionToken: string;
|
|
sessionEpoch: string;
|
|
accountEpoch: string;
|
|
releaseEpoch: string;
|
|
generation: number;
|
|
}>;
|
|
```
|
|
|
|
- token은 충분한 entropy를 가진 opaque value다.
|
|
- email, account/tenant/user ID, domain object ID를 token 또는 physical key에
|
|
직접 넣지 않는다.
|
|
- 낮은 entropy의 account ID를 frontend에서 단순 hash한 값을 opaque token으로
|
|
간주하지 않는다.
|
|
- current session owner 또는 composition authority가 scope를 발급한다.
|
|
- memory query key, persisted binding과 invalidation event는 같은 frozen source
|
|
snapshot에서 query profile이 선택한 `ORIGIN_SHARED`, `ACCOUNT_BOUND` 또는
|
|
`SESSION_BOUND` projection/fingerprint를 계산한다. 서로 다른 projection에
|
|
raw session/account epoch를 무조건 복사하지 않는다.
|
|
- `generation`은 page-local lifecycle fence이며 backend wire/entity revision이
|
|
아니다. 모든 async terminal write 전에 captured generation을 다시 검증한다.
|
|
|
|
`sessionEpoch`, `accountEpoch`, `releaseEpoch`는 서로 다른 폐기 이유를 표현한다.
|
|
|
|
| epoch | 변경 조건 | 폐기 범위 |
|
|
| --- | --- | --- |
|
|
| session | sign-in/re-auth/session 교체 | 기존 runtime의 in-flight/result/cache |
|
|
| account | account/tenant 전환, logout/account deletion | account partition 전체 |
|
|
| release | query-key/codec/API compatibility가 깨지는 release | incompatible persisted cache |
|
|
|
|
epoch는 backend entity version이 아니다. business conflict 해결이나 optimistic
|
|
locking에 사용하지 않는다.
|
|
|
|
### 4.2 query scope/persistence registry
|
|
|
|
query key factory와 함께 immutable scope/persistence profile을 등록한다.
|
|
freshness, GC, refetch, retry, result budget, pagination과 conditional policy는
|
|
[VD-25](./decisions/VD-25-server-state-cache-lifecycle.md)의
|
|
`ServerStateProfile`이 유일하게 소유한다.
|
|
|
|
```ts
|
|
type QueryScopePersistencePolicy = Readonly<{
|
|
policyId: string;
|
|
namespace: readonly [string, number];
|
|
keySchemaVersion: number;
|
|
classification: "PUBLIC" | "INTERNAL" | "PERSONAL" | "CONFIDENTIAL";
|
|
scope: "ORIGIN_SHARED" | "ACCOUNT_BOUND" | "SESSION_BOUND";
|
|
persistence:
|
|
| Readonly<{ kind: "MEMORY_ONLY" }>
|
|
| Readonly<{
|
|
kind: "INDEXEDDB";
|
|
profileId: string;
|
|
maxAgeMs: number;
|
|
maxEntryBytes: number;
|
|
}>;
|
|
crossTab: "NONE" | "INVALIDATE";
|
|
invalidationTopics: readonly Readonly<{
|
|
topicId: string;
|
|
topicVersion: number;
|
|
}>[];
|
|
}>;
|
|
```
|
|
|
|
검증 규칙:
|
|
|
|
- persistence `maxAgeMs <=` 승인된 retention
|
|
- persistence 사용 시 joined VD-25 profile의 `gcTimeMs`, restore와 hydration
|
|
retention이 모순되지 않게 구성한다.
|
|
- `PERSONAL` persistence에는 `ACCOUNT_BOUND` 이상, logout purge와 explicit approval이
|
|
필수다.
|
|
- `CONFIDENTIAL`과 credential은 persistence 등록을 거절한다.
|
|
- `NONE`은 topic 0개, `INVALIDATE`는 namespace당 unique topic 1..8개이고
|
|
`(topicId, topicVersion)`당 namespace fan-out은 32개 이하로 제한한다.
|
|
- joined VD-25 profile의 topic set/version과 exact match한다.
|
|
- policy object, nested allowlist와 codec은 construction 시 deep snapshot/freeze한다.
|
|
|
|
### 4.3 query key
|
|
|
|
normative key layout:
|
|
|
|
```text
|
|
[
|
|
"query",
|
|
keySchemaVersion,
|
|
scopeFingerprint,
|
|
namespaceName,
|
|
namespaceVersion,
|
|
queryDefinitionVersion,
|
|
canonicalSemanticInput
|
|
]
|
|
```
|
|
|
|
규칙:
|
|
|
|
- VD-25는 이 배열을 재정의하지 않고 definition/pagination projection을 채운다.
|
|
- query function이 의존하는 모든 non-secret 변수를 key에 포함한다.
|
|
- token, URL 전체, authorization, email, filename과 raw personal label을 넣지
|
|
않는다.
|
|
- object key ordering은 canonicalize하되 cycle, function, symbol, `BigInt`,
|
|
non-finite number, DOM/native object를 fail-closed로 거절한다.
|
|
- array 순서는 의미가 있으므로 유지한다.
|
|
- query namespace는 arbitrary caller string이 아니라 registry reference다.
|
|
- cross-tab event에는 전체 query key나 filter를 넣지 않는다.
|
|
|
|
현재 `src/contracts/query-keys.ts`의 `canonicalize()`는 object key order는
|
|
정규화하지만 cycle과 non-serializable input을 닫지 않는다. production registry는
|
|
이를 검증하는 codec을 추가해야 한다.
|
|
|
|
### 4.4 Web Storage registry
|
|
|
|
각 logical key는 최소 다음을 갖는다.
|
|
|
|
```ts
|
|
type WebStorageDefinition<Value> = Readonly<{
|
|
logicalName: string;
|
|
backend: "localStorage" | "sessionStorage";
|
|
scope: "ORIGIN_SHARED" | "OPAQUE_PARTITION" | "TAB";
|
|
classification: "PUBLIC_PREFERENCE" | "OPAQUE_CONTROL";
|
|
schemaVersion: number;
|
|
maxSerializedBytes: number;
|
|
retention:
|
|
| Readonly<{ kind: "SESSION" }>
|
|
| Readonly<{ kind: "TTL"; maxAgeMs: number }>
|
|
| Readonly<{ kind: "EXPLICIT_DELETE" }>;
|
|
valueCodec: string;
|
|
migration: "DISCARD" | Readonly<{ fromVersion: number; migrate(value: unknown): unknown }>;
|
|
quotaFallback: "MEMORY" | "NO_PERSIST" | "FEATURE_DISABLE";
|
|
logoutAction: "KEEP" | "PURGE_PARTITION";
|
|
}>;
|
|
```
|
|
|
|
현재 registry는 이 목표 계약 중 backend, classification, schema version,
|
|
`valueCodec`, TTL, migration 선언과 quota fallback을 구현한다.
|
|
`valueCodec`은 closed ID이며 `isStorageValueAllowed()`가 ID별 값을 검증한다.
|
|
key별 byte cap, opaque account partition과 logout action은 아직 추가되지 않았다.
|
|
|
|
- 모든 read/write/remove는 logical registry reference를 받는다.
|
|
- physical key는 application ID, environment, opaque partition, logical namespace와
|
|
schema version에서 결정적으로 파생한다.
|
|
- 다른 application key를 열거하거나 origin 전체 `clear()`를 호출하지 않는다.
|
|
- migration은 registry에 명시된 인접/지원 version만 수행한다.
|
|
- reconstructable control record의 corrupt/unknown/future version은 miss로
|
|
격하하고 exact key만 best-effort 제거한다.
|
|
- stored `undefined`와 miss를 구분할 수 있도록 read 결과는 `HIT`/`MISS`를
|
|
명시한다.
|
|
|
|
## 5. TanStack Query memory cache
|
|
|
|
### 5.1 기본 정책
|
|
|
|
memory cache는 기본이고 persistence는 기본이 아니다.
|
|
|
|
- runtime마다 새 `QueryClient`를 만든다.
|
|
- module singleton을 만들지 않는다.
|
|
- SSR을 도입하면 HTTP request마다 새 client를 만들고 request 종료 시 폐기한다.
|
|
- HTTP transport가 bounded retry를 소유하므로 현재 기본 query/mutation retry
|
|
`false`를 유지한다.
|
|
- global stale/gc 값은 안전한 baseline일 뿐이다. 실제 query는 registry profile을
|
|
사용한다.
|
|
- `refetchOnWindowFocus`와 reconnect는 freshness safety net이지만 authorization
|
|
또는 cross-tab delivery 보장은 아니다.
|
|
- active refetch failure가 cached data를 지우지 않도록 initial failure와 stale
|
|
background failure를 구분한다.
|
|
- mapper가 만든 cached value는 immutable하게 취급한다.
|
|
|
|
### 5.2 freshness, retention과 invalidation
|
|
|
|
- fresh query도 권한 폐기 또는 account reset 시 반드시 remove/clear한다.
|
|
- `invalidateQueries`는 matching query를 stale로 만들고 active query를 refetch할
|
|
수 있지만 기존 data를 즉시 제거하지 않는다.
|
|
- `removeQueries`는 민감 data, 권한 축소와 account boundary에서 사용한다.
|
|
- `resetQueries`는 query를 initial state로 되돌리고 active query를 refetch할 수
|
|
있으므로 scope disposal의 대체가 아니다.
|
|
- `queryClient.clear()`는 exact old runtime 전체를 폐기할 때만 사용한다.
|
|
- ordinary mutation은 namespace invalidate가 기본이다.
|
|
- delete/permission-revocation처럼 stale data 표시 자체가 위험하면 exact
|
|
`remove` 후 필요한 namespace를 invalidate한다.
|
|
|
|
### 5.3 memory bound
|
|
|
|
TanStack의 `gcTime`만으로 active query의 memory를 hard bound할 수 없다.
|
|
|
|
- gateway/mapper에서 response item/count/byte limit을 검증한다.
|
|
- File, Blob, object URL, large binary와 unbounded collection을 query cache에
|
|
넣지 않는다.
|
|
- inactive query에는 finite `gcTime`을 둔다.
|
|
- query entry count/estimated payload pressure를 safe bucket으로 관측한다.
|
|
- hard memory cap이 필요한 제품은 approved query class별 bounded eviction
|
|
controller를 추가한다. active query를 임의 삭제하는 global timer를 기본
|
|
skeleton에 두지 않는다.
|
|
|
|
### 5.4 scope transition과 late result fence
|
|
|
|
account/session 전환은 query key prefix만 바꾸는 것으로 끝나지 않는다.
|
|
|
|
1. old runtime에 신규 query/mutation admission을 중지한다.
|
|
2. scope generation을 `FENCED`로 바꾼다.
|
|
3. old client의 query를 cancel하고 provider/controller를 detach한다.
|
|
4. old `QueryClient`를 clear/dispose한다.
|
|
5. old scope의 channel과 persistence connection을 닫는다.
|
|
6. exact old partition lifecycle purge를 시작한다.
|
|
7. 새 scope와 새 `QueryClient`로 provider를 remount한다.
|
|
|
|
가능하면 scope마다 별도 `QueryClient` instance를 사용한다. old async result가
|
|
늦게 resolve되어도 새 client에 쓸 수 없다. mutation transport가 이미 server로
|
|
전달된 경우 frontend cancel은 server side effect를 되돌리지 않는다. old mutation
|
|
완료 callback은 generation mismatch로 UI/cache update를 폐기하고, server 결과는
|
|
새 session의 정상 revalidation에서 다시 확인한다.
|
|
|
|
## 6. Local Storage와 Session Storage
|
|
|
|
### 6.1 용도
|
|
|
|
`localStorage`:
|
|
|
|
- 작은 공개 UI preference
|
|
- release 또는 cache invalidation의 opaque pulse/epoch
|
|
- 명시적으로 승인된 reconstructable control record
|
|
|
|
`sessionStorage`:
|
|
|
|
- chunk reload guard
|
|
- 현재 tab의 opaque instance ID
|
|
- reload를 넘어 유지해야 하는 작은 tab-local control state
|
|
|
|
금지:
|
|
|
|
- server response collection persistence
|
|
- queue, counter, lock 또는 cross-tab CAS
|
|
- 인증 credential
|
|
- 큰 form draft와 file/blob
|
|
- arbitrary JSON dump
|
|
- logout을 sessionStorage tab 종료에 의존
|
|
|
|
Web Storage는 동기식이므로 serialization을 포함한 operation이 main thread를
|
|
막는다. 등록 key마다 작은 byte hard cap을 강제하고 bulk scan이나 큰 value를
|
|
저장하지 않는다. 큰 구조화 data는 IndexedDB를 사용한다.
|
|
|
|
### 6.2 sessionStorage caveat
|
|
|
|
`sessionStorage`는 origin과 top-level browsing context로 분리되고 reload에는
|
|
남지만 다른 tab과 공유되지 않는다. opener가 있는 새 window는 생성 시 opener의
|
|
sessionStorage snapshot을 복사할 수 있다. 이후 변경은 공유되지 않더라도 copied
|
|
secret/control state에 의존하면 안 된다. 새 window가 독립 session이어야 하면
|
|
`noopener`/관련 browser policy와 새 tab instance ID를 사용한다.
|
|
|
|
sessionStorage의 `storage` event는 같은 top-level context의 iframe에는 전달될 수
|
|
있지만 다른 tab invalidation transport로 사용할 수 없다.
|
|
|
|
### 6.3 envelope와 serialization
|
|
|
|
현재 구현 envelope:
|
|
|
|
```ts
|
|
type BrowserStorageEnvelope = Readonly<{
|
|
schemaVersion: number;
|
|
expiresAt: number | null;
|
|
value: unknown;
|
|
}>;
|
|
```
|
|
|
|
opaque account partition을 선택할 때는 새 envelope version으로 scope fingerprint와
|
|
필요한 write metadata를 추가해야 한다. 기존 세 필드 envelope에 의미를 바꾸어
|
|
끼워 넣지 않는다.
|
|
|
|
- codec encode 결과가 JSON-safe인지 먼저 검증한다.
|
|
- `JSON.stringify`가 변경하는 `undefined`, `NaN`, infinity, sparse array와
|
|
지원하지 않는 `BigInt`, cycle을 암묵적으로 허용하지 않는다.
|
|
- serialized UTF-8/보수적 UTF-16 byte estimate가 hard cap을 넘으면 native
|
|
storage를 호출하지 않는다.
|
|
- 현재 read는 JSON parse 뒤 exact envelope, schema, TTL, key별 value codec
|
|
순으로 검증한다. scope를 추가한 version은 codec 전에 exact scope도 검증한다.
|
|
- 미래 시각이나 비정상적으로 먼 expiry는 corrupt/clock-skew policy에 따라
|
|
fail-closed miss로 처리한다.
|
|
- TTL은 read visibility rule이다. expired bytes가 실제로 제거됐음을 보장하지
|
|
않으므로 bounded sweep을 별도로 둔다.
|
|
|
|
memory fallback도 같은 envelope, TTL, codec와 scope를 사용해야 한다. raw
|
|
value만 Map에 넣으면 persistent backend에서 만료된 뒤 memory copy가 다시 보이는
|
|
문제가 생긴다. 현재 adapter는 동일 serialized envelope를 memory overlay에도
|
|
사용하므로 이 불변조건을 유지해야 한다.
|
|
|
|
### 6.4 write outcome
|
|
|
|
fallback 성공과 persistence 성공을 같은 `{ ok: true }`로 숨기거나, fallback이
|
|
사용 가능함에도 단순 `{ ok: false }`만 반환하지 않는다.
|
|
|
|
```ts
|
|
type StorageWriteOutcome =
|
|
| Readonly<{ ok: true; durability: "PERSISTED" }>
|
|
| Readonly<{ ok: true; durability: "MEMORY_ONLY"; degraded: true }>
|
|
| Readonly<{ ok: false; error: ClientStorageFailure }>;
|
|
```
|
|
|
|
key policy가 persistence를 반드시 요구하면 memory fallback은 실패다. 공개 theme
|
|
preference처럼 current runtime 사용이 가능하면 `MEMORY_ONLY` degraded success를
|
|
허용할 수 있다. caller가 이 결정을 하지 않고 registry policy가 소유한다.
|
|
|
|
### 6.5 exception과 cleanup
|
|
|
|
- `Storage` property access, get, set, remove 모두 `SecurityError` 등으로 throw할 수
|
|
있다.
|
|
- quota zero/private mode, user policy와 embedded context를 지원 가능 여부와
|
|
분리해 관측한다.
|
|
- `QuotaExceededError`에서 reconstructable exact-key cleanup을 한 뒤 동일
|
|
idempotent write를 최대 한 번만 재시도한다.
|
|
- corrupt/expired read의 exact-key 제거 실패가 validated miss를 raw exception으로
|
|
바꾸지 않게 한다. cleanup failure는 별도 degraded observation이다.
|
|
- origin-wide `clear()`를 recovery로 호출하지 않는다.
|
|
|
|
## 7. IndexedDB query persistence
|
|
|
|
### 7.1 기본 OFF
|
|
|
|
TanStack cache persistence는 기본적으로 끈다. 현재
|
|
`STORAGE_REGISTRY.QUERY_PERSISTENCE`가 `disabled`와
|
|
`sensitive-forbidden`인 것은 이 기본값을 표현한다. persistence를 구현한다는
|
|
이유로 이를 localStorage key로 바꾸지 않는다.
|
|
|
|
다음 조건을 모두 충족한 query profile만 별도 IndexedDB persistence registry에
|
|
등록한다.
|
|
|
|
- server-authoritative, reconstructable data
|
|
- codec와 query-key schema가 고정됨
|
|
- classification/partition/retention owner 승인
|
|
- entry와 dataset byte budget 존재
|
|
- offline 또는 warm-start 가치가 측정됨
|
|
- logout/reset와 release busting이 정의됨
|
|
- three-engine native contract evidence 존재
|
|
|
|
### 7.2 기존 IndexedDB runtime 재사용 경계
|
|
|
|
기존 runtime에서 재사용한다.
|
|
|
|
- opaque dataset scope와 policy binding
|
|
- transaction-complete success
|
|
- schema migration과 record codec migration
|
|
- blocked/versionchange/forced close
|
|
- revision CAS와 idempotency
|
|
- byte budget, retention metadata와 bounded sweep
|
|
- lifecycle authority와 exact partition purge
|
|
- closed failure와 native exception redaction
|
|
|
|
query persistence facade가 추가로 소유한다.
|
|
|
|
- TanStack query namespace와 key codec
|
|
- persist allowlist
|
|
- dehydrated projection을 stable wire record로 변환
|
|
- scope/cache epoch/release buster binding
|
|
- restore ordering과 hydration
|
|
- query-specific max age와 remove policy
|
|
|
|
application/domain에는 TanStack type이나 IndexedDB store/index name을 노출하지
|
|
않는다.
|
|
|
|
### 7.3 저장 형식
|
|
|
|
TanStack의 내부 cache object 전체를 검증 없이 저장하지 않는다. 최소 record:
|
|
|
|
```ts
|
|
type PersistedQueryRecord = Readonly<{
|
|
recordVersion: 1;
|
|
queryHash: string;
|
|
encodedQueryKey: unknown;
|
|
policyId: string;
|
|
scopeFingerprint: string;
|
|
releaseEpoch: string;
|
|
namespaceEpoch: number;
|
|
dataUpdatedAtEpochMs: number;
|
|
persistedAtEpochMs: number;
|
|
expiresAtEpochMs: number;
|
|
payloadCodecVersion: number;
|
|
payload: unknown;
|
|
measuredBytes: number;
|
|
revision: number;
|
|
}>;
|
|
```
|
|
|
|
query key와 payload는 각각 codec을 통과한다. error, pending mutation, function,
|
|
Promise, `AbortSignal`, File/Blob/native handle, object URL, capability와 credential은
|
|
저장하지 않는다.
|
|
|
|
### 7.4 restore gate
|
|
|
|
restore 순서:
|
|
|
|
1. bounded deadline으로 database를 연다.
|
|
2. immutable dataset binding을 검증한다.
|
|
3. exact profile-selected `scopeFingerprint`와 `releaseEpoch`를 검증한다.
|
|
session/account epoch는 `ACCOUNT_BOUND`/`SESSION_BOUND` projection에 포함될
|
|
때만 검증한다.
|
|
4. record/envelope와 codec version을 검증한다.
|
|
5. TTL과 namespace invalidation epoch를 검증한다.
|
|
6. byte/count cap 안에서 decode한다.
|
|
7. approved query profile만 hydrate한다.
|
|
8. active query는 normal stale/refetch policy를 따른다.
|
|
|
|
expired, busted, wrong-scope, corrupt reconstructable record는 UI error가 아니라
|
|
cache miss다. exact record 또는 exact cache dataset만 bounded cleanup한다. 다른
|
|
feature DB나 origin storage를 삭제하지 않는다.
|
|
|
|
restore가 boot를 무한히 막지 않도록 deadline을 둔다. query persistence가 optional
|
|
이면 timeout, unavailable, blocked에서 빈 memory cache로 fail open하고
|
|
`ONLINE_ONLY` degraded observation을 남긴다. offline-required 제품은 별도
|
|
feature repository와 명시적 UX가 필요하므로 query persistence fallback으로
|
|
가장하지 않는다.
|
|
|
|
### 7.5 SSR/hydration
|
|
|
|
- server process에서 browser IndexedDB/Web Storage/BroadcastChannel에 접근하지
|
|
않는다.
|
|
- SSR QueryClient는 request-scoped다. singleton은 사용자 간 data leak을 만든다.
|
|
- server dehydration도 approved successful query만 포함한다.
|
|
- browser persisted state가 최신 SSR payload를 덮지 않는다.
|
|
- merge가 필요하면 server response/version을 우선하고, 없는 approved query만
|
|
restore하거나 명시적 server revision 비교를 사용한다.
|
|
- browser storage read 때문에 initial server/client markup이 달라지지 않게
|
|
bootstrap 또는 hydration-safe provider 단계에서 restore한다.
|
|
|
|
### 7.6 multi-tab writer
|
|
|
|
여러 tab이 하나의 full QueryClient snapshot key를 last-write-wins로 덮어쓰면
|
|
오래된 tab이 invalidated data를 다시 살릴 수 있다. 이 reference 설계는
|
|
**shared per-query record + monotonic durable namespace ledger/CAS** 하나만
|
|
선택한다.
|
|
|
|
tab별 snapshot partition은 restore 의미가 달라지고, single-writer coordinator는
|
|
leader loss/fencing/takeover라는 별도 protocol이 필요하므로 검토했지만 이
|
|
reference runtime에서는 선택하지 않는다. 다른 writer model을 도입하려면
|
|
VD-13 amendment와 동등한 resurrection/race/promotion evidence가 필요하다.
|
|
|
|
단순 localStorage lock이나 best-effort BroadcastChannel election을 correctness
|
|
fence로 사용하지 않는다. shared ledger가 필요 없으면 persistence 자체를
|
|
조립하지 않는다.
|
|
|
|
### 7.7 write batching과 shutdown
|
|
|
|
- cache writes는 bounded debounce/coalescing을 적용한다.
|
|
- 동시 save는 serialize하고 superseded snapshot은 쓰지 않는다.
|
|
- 각 transaction은 필요한 store만 열고 arbitrary network/async callback을
|
|
transaction 중간에 await하지 않는다.
|
|
- request success가 아니라 transaction `complete`에서 persisted 성공을 확정한다.
|
|
- `pagehide`, `beforeunload`, browser 종료에서 새 IndexedDB transaction 완료를
|
|
보장한다고 가정하지 않는다.
|
|
- normal runtime 중 주기적으로 저장하고 unload write는 best-effort 보조로만 둔다.
|
|
- `dispose()`는 pending timer를 취소하고 connection/listener를 닫는다. 아직
|
|
commit되지 않은 write를 persisted success로 기록하지 않는다.
|
|
|
|
## 8. cache epoch와 durable invalidation ledger
|
|
|
|
### 8.1 epoch 종류
|
|
|
|
```ts
|
|
type DurableCacheLedger = Readonly<{
|
|
ledgerVersion: 1;
|
|
scopeFingerprint: string;
|
|
releaseEpoch: string;
|
|
namespaces: Readonly<Record<string, number>>;
|
|
revision: number;
|
|
}>;
|
|
```
|
|
|
|
- session/account epoch는 profile이 `ACCOUNT_BOUND`/`SESSION_BOUND`를 선택한
|
|
경우에만 frozen `CacheScopeSnapshot`에서 `scopeFingerprint`로 binding하며 raw
|
|
epoch를 ledger wire에 중복 저장하지 않는다. `ORIGIN_SHARED` fingerprint에는
|
|
session/account epoch를 넣지 않는다.
|
|
- scope fingerprint/release epoch mismatch는 해당 scope record를 hydrate하지 않는다.
|
|
- namespace epoch는 ordinary mutation invalidation과 persisted record resurrection
|
|
방지에 사용한다.
|
|
- counter 증가와 persisted query mutation을 함께 해야 하면 같은 IndexedDB
|
|
transaction에 둔다.
|
|
- localStorage read-modify-write counter는 cross-tab atomic하지 않으므로 durable
|
|
monotonic ledger로 사용하지 않는다.
|
|
|
|
### 8.2 release buster
|
|
|
|
release epoch는 모든 deploy마다 무조건 바꿀 필요는 없다. 다음 compatibility 중
|
|
하나가 깨질 때 올린다.
|
|
|
|
- query key schema
|
|
- payload codec
|
|
- API/mapper meaning
|
|
- scope binding
|
|
- persistence record format
|
|
- 지원하는 old reader/writer window
|
|
|
|
안전 우선 배포는 build/release ID를 buster로 사용해 매 deploy cache를 버릴 수
|
|
있지만 warm-start 효율이 낮다. 호환 epoch를 유지하려면 N-1 reader/writer
|
|
contract와 rollback evidence가 필요하다.
|
|
|
|
### 8.3 lost hint 이후 안전성
|
|
|
|
Broadcast hint가 손실되어도:
|
|
|
|
- finite staleTime/focus/reconnect가 eventual revalidation을 제공한다.
|
|
- persisted restore는 durable namespace epoch보다 오래된 record를 거절한다.
|
|
- visibility/focus 시 bounded ledger refresh를 선택할 수 있다.
|
|
|
|
즉시 global consistency가 업무 invariant라면 browser invalidation bus만으로
|
|
충족할 수 없다. backend revision/ETag, server event stream 또는 업무별 sync
|
|
protocol을 추가한다.
|
|
|
|
## 9. 탭 간 invalidation
|
|
|
|
### 9.1 목적과 비목적
|
|
|
|
목적:
|
|
|
|
- 한 tab의 committed mutation 뒤 다른 tab의 active query를 빠르게 stale 처리
|
|
- session/account/release epoch가 달라졌을 가능성을 알리는 non-destructive
|
|
revalidation hint 전달
|
|
- IndexedDB versionchange/maintenance prepare hint 전달
|
|
|
|
비목적:
|
|
|
|
- query payload/state replication
|
|
- authorization, logout 또는 server commit 증명
|
|
- exactly-once delivery
|
|
- distributed lock/leader election
|
|
- business event bus
|
|
- offline command transport
|
|
|
|
TanStack의 experimental broadcast client처럼 QueryClient state 자체를 tab 사이에
|
|
복제하는 기능은 기본 선택하지 않는다. protocol 안정성뿐 아니라 query payload와
|
|
scope가 broadcast boundary를 넘고, multi-tab persistence resurrection 문제가
|
|
커지기 때문이다. 이 template은 좁은 namespace invalidation protocol을 소유한다.
|
|
|
|
### 9.2 event envelope
|
|
|
|
```ts
|
|
type CrossTabCacheEvent = Readonly<{
|
|
protocolVersion: 1;
|
|
eventId: string;
|
|
sourceId: string;
|
|
sourceEpoch: string;
|
|
sequence: number;
|
|
cacheEpoch: string;
|
|
emittedAt: number;
|
|
expiresAt: number;
|
|
topic: string;
|
|
topicVersion: number;
|
|
}>;
|
|
```
|
|
|
|
목표 설계에서 `cacheEpoch`는 exact session/account/release scope의 opaque
|
|
compatibility fingerprint다. 현재 값은 release ID에만 묶여 있다. 원래 ID나 각
|
|
epoch의 의미 값을 wire에 싣지 않는다. event에는 query data, query args/filter,
|
|
domain ID, user/tenant ID, URL, token, error message와 stack을 넣지 않는다.
|
|
`topic`은 registry-owned opaque/safe identifier고 receiver가 local registry를
|
|
통해 TanStack namespace로 해석한다.
|
|
|
|
receiver validation:
|
|
|
|
- exact protocol version
|
|
- bounded serialized size
|
|
- registry-known topic/version
|
|
- exact composite cache epoch
|
|
- valid UUID/opaque source와 sequence
|
|
- reasonable issue/expiry time
|
|
- self source drop
|
|
- bounded LRU event ID duplicate drop
|
|
- unknown field/version/topic fail-closed drop
|
|
|
|
duplicate와 out-of-order invalidation은 안전해야 한다. namespace epoch가 있으면
|
|
durable ledger를 다시 읽어 현재보다 큰 epoch만 적용한다. wire event 자체의
|
|
wall clock이나 source-local sequence를 global ordering으로 해석하지 않는다.
|
|
|
|
### 9.3 transport
|
|
|
|
기본 우선순위:
|
|
|
|
```text
|
|
BroadcastChannel
|
|
-> unavailable/failure
|
|
-> dedicated localStorage pulse + window storage event
|
|
-> unavailable/failure
|
|
-> local-only cache action + normal focus/stale revalidation
|
|
```
|
|
|
|
`BroadcastChannel`:
|
|
|
|
- same origin만으로 충분하다고 가정하지 않고 exact storage partition/scope를
|
|
검증한다.
|
|
- channel name은 application과 protocol major를 포함한다.
|
|
- channel construction/postMessage/message parsing 모두 실패할 수 있다.
|
|
- `close()`를 반드시 호출한다.
|
|
|
|
localStorage pulse fallback:
|
|
|
|
- dedicated registered physical key 하나만 사용한다.
|
|
- unique event envelope를 `setItem`하고 필요하면 exact key를 best-effort 제거한다.
|
|
- publishing tab에는 `storage` event가 오지 않으므로 local action은 publisher가
|
|
직접 수행한다.
|
|
- receiver는 exact key, storage area, scope와 envelope를 검증한다.
|
|
- localStorage write success는 다른 tab delivery acknowledgement가 아니다.
|
|
- quota/security failure는 local mutation을 실패시키지 않는다.
|
|
- sessionStorage event는 다른 tab에 전달되지 않으므로 fallback으로 쓰지 않는다.
|
|
|
|
transport selection과 fallback은 capability detection 결과를 snapshot하되,
|
|
probe 결과를 영구 availability 보장으로 간주하지 않는다.
|
|
|
|
### 9.4 receiver 동작
|
|
|
|
정상 delivery는 registry topic을 local namespace로 해석해 invalidate하고 active
|
|
query만 bounded refetch한다. source sequence gap이 보이면 특정 payload를
|
|
신뢰하지 않고 등록 namespace 전체를 stale 처리하거나 durable ledger를 다시
|
|
읽는다.
|
|
|
|
remote hint는 `removeQueries`, `resetQueries`, `queryClient.clear()`를 직접
|
|
호출할 권한이 없다. remove/reset/clear는 현재 tab의 session owner, release
|
|
coherence check 또는 사용자가 시작한 local lifecycle처럼 검증된 local authority만
|
|
호출한다. delete mutation을 실행한 tab은 위험한 exact detail을 local remove할 수
|
|
있지만, 다른 tab에는 namespace invalidation만 보내 서버에서 `NOT_FOUND` 또는
|
|
새 권한 상태를 다시 확인하게 한다.
|
|
|
|
receiver는 event burst를 namespace별로 coalesce하고 bounded queue를 사용한다.
|
|
cross-tab fan-out으로 모든 inactive query를 즉시 refetch해 thundering herd를 만들지
|
|
않는다. active query만 refetch하고 inactive query는 다음 mount에서 fetch한다.
|
|
같은 tab의 동일 query는 TanStack dedup을 사용하되, tab 간 network dedup을
|
|
가정하지 않는다.
|
|
|
|
### 9.5 logout은 broadcast에 의존하지 않는다
|
|
|
|
현재 tab의 logout/reset은 local session owner notification이 authoritative하다.
|
|
다른 tab도 external auth owner/cookie/session 상태 변화를 자체적으로 감지하고
|
|
보호된 request에서 재검증해야 한다. cache invalidation hint는 이를 빠르게 발견할
|
|
수 있지만 remote event 하나가 전체 cache를 파괴하거나, event loss 때문에 logout이
|
|
무효가 되어서는 안 된다.
|
|
|
|
## 10. lifecycle
|
|
|
|
### 10.1 boot
|
|
|
|
1. runtime config와 release contract를 검증한다.
|
|
2. session owner에서 immutable cache scope를 얻는다.
|
|
3. scope별 QueryClient를 만든다.
|
|
4. persistence가 선택된 경우 bounded restore를 수행한다.
|
|
5. restore result를 compatibility/TTL/codec로 검증한다.
|
|
6. provider tree를 mount한다.
|
|
7. cross-tab runtime이 선택된 경우 listener를 시작한다.
|
|
|
|
optional persistence 때문에 boot가 영구 blocked되지 않게 deadline과
|
|
memory-only fallback을 둔다. listener를 restore보다 먼저 시작해야 한다면 수신
|
|
event를 bounded queue에 두고 scope validation 완료 전에는 적용하지 않는다.
|
|
|
|
### 10.2 logout/account switch
|
|
|
|
```text
|
|
SESSION/ACCOUNT_TRANSITION_REQUESTED
|
|
-> old generation FENCED
|
|
-> new network/cache admission stopped
|
|
-> old query cancellation + provider detach
|
|
-> old QueryClient remove/clear
|
|
-> old persistence writer stopped
|
|
-> exact Web Storage key purge
|
|
-> exact IndexedDB partition lifecycle purge
|
|
-> optional non-destructive epoch/invalidation hint best-effort publish
|
|
-> channel/DB/timer/listener dispose
|
|
-> new opaque scope + new runtime
|
|
```
|
|
|
|
- old scope purge와 new scope open을 혼합하지 않는다.
|
|
- old physical data가 crash 때문에 남아도 new scope binding이 이를 읽지 못해야
|
|
한다.
|
|
- account deletion은 bounded purge progress와 recovery를 별도 표면으로 제공할 수
|
|
있다.
|
|
- confidential data를 browser에 persist했다면 “eventual cleanup”만으로 충분하지
|
|
않을 수 있으므로 애초 persistence를 금지한다.
|
|
- origin 전체 localStorage/IndexedDB를 지우지 않는다.
|
|
|
|
### 10.3 page lifecycle
|
|
|
|
- `visibilitychange`/focus는 stale query와 optional invalidation ledger 재검사
|
|
trigger다.
|
|
- `pagehide`는 best-effort flush/close trigger일 뿐 commit 보장이 아니다.
|
|
- bfcache restore에서 source instance/generation과 listener 중복을 확인한다.
|
|
- React StrictMode mount/unmount 반복에도 subscription/channel/timer가 하나만
|
|
남아야 한다.
|
|
- dispose는 idempotent해야 한다.
|
|
|
|
### 10.4 release transition
|
|
|
|
새 release가 incompatible epoch를 발표하면:
|
|
|
|
1. old query admission을 fence한다.
|
|
2. incompatible memory/persisted cache를 remove한다.
|
|
3. old persistence writer와 channel을 dispose한다.
|
|
4. 현재 release recovery policy에 따라 reload 또는 새 runtime 생성으로 전환한다.
|
|
|
|
old tab이 IndexedDB schema upgrade를 막으면 existing IndexedDB
|
|
blocked/versionchange UX를 사용한다. Broadcast prepare hint는 참고용이고 실제
|
|
connection close/transaction state가 authority다.
|
|
|
|
## 11. quota, timeout과 resource cleanup
|
|
|
|
### 11.1 budget
|
|
|
|
| layer | 필수 limit |
|
|
| --- | --- |
|
|
| memory query | response count/byte, inactive gc, optional entry pressure |
|
|
| Web Storage | key별 serialized bytes, 전체 등록 key count |
|
|
| IndexedDB query persistence | entry bytes, dataset soft/hard bytes, record count, restore bytes/count |
|
|
| invalidation bus | event bytes, queue length, duplicate LRU, coalesce window |
|
|
|
|
`navigator.storage.estimate()`는 origin 전체의 rough signal이다. Web Storage,
|
|
IndexedDB, OPFS와 Cache Storage별 free-space reservation으로 해석하지 않는다.
|
|
실제 `QuotaExceededError`가 authoritative failure다.
|
|
|
|
query cache는 reconstructable이므로 pressure에서 expired/oldest/inactive
|
|
persisted record를 bounded batch로 제거할 수 있다. 기존 IndexedDB의
|
|
user-authored/`UNTIL_SYNCED` dataset과 cleanup 범위를 공유하지 않는다.
|
|
|
|
### 11.2 deadline과 cancellation
|
|
|
|
- IndexedDB open/restore/maintenance에는 monotonic deadline과 `AbortSignal`을
|
|
전달한다.
|
|
- Web Storage는 synchronous이므로 큰 operation 자체를 byte/count limit으로
|
|
금지한다.
|
|
- cross-tab receiver가 시작한 refetch는 current query cancellation policy를
|
|
따른다.
|
|
- shutdown cleanup은 제품 request deadline과 분리된 짧은 cleanup budget을
|
|
가질 수 있다.
|
|
- deadline 초과와 caller cancellation을 같은 실패 code로 합치지 않는다.
|
|
|
|
### 11.3 retry owner
|
|
|
|
- HTTP retry owner가 transport면 Query retry는 끈다.
|
|
- Web Storage quota cleanup 뒤 write retry는 최대 1회다.
|
|
- IndexedDB transaction은 effect가 확실히 `NOT_APPLIED`이고 operation이
|
|
idempotent/CAS-protected일 때만 bounded retry한다.
|
|
- channel publish는 자동 무한 retry하지 않는다. stale/focus safety net으로
|
|
degrade한다.
|
|
- application, Query, HTTP SDK, service worker가 동시에 retry하지 않는다.
|
|
|
|
### 11.4 cleanup 대상
|
|
|
|
- QueryClient observer/cache/mutation cache
|
|
- Query persistence debounce timer와 in-flight writer
|
|
- IndexedDB connection과 versionchange handler
|
|
- BroadcastChannel
|
|
- `window.storage`, focus, visibility, online listener
|
|
- duplicate LRU와 pending invalidation queue
|
|
- memory fallback envelope
|
|
- exact registered expired Web Storage keys
|
|
|
|
cleanup 실패를 성공으로 숨기지 않되, diagnostics failure가 cleanup을 막지 않게
|
|
한다.
|
|
|
|
## 12. 공통 failure model
|
|
|
|
### 12.1 closed failure
|
|
|
|
```ts
|
|
type ClientCacheFailure = Readonly<{
|
|
code:
|
|
| "ABORTED"
|
|
| "DEADLINE_EXCEEDED"
|
|
| "UNAVAILABLE"
|
|
| "UNSUPPORTED"
|
|
| "POLICY_REJECTED"
|
|
| "SERIALIZATION_FAILED"
|
|
| "CORRUPT_DATA"
|
|
| "VERSION_MISMATCH"
|
|
| "SCOPE_MISMATCH"
|
|
| "QUOTA_EXCEEDED"
|
|
| "BLOCKED"
|
|
| "CONFLICT"
|
|
| "DELIVERY_DROPPED"
|
|
| "INTERNAL";
|
|
operation:
|
|
| "MEMORY_READ"
|
|
| "MEMORY_WRITE"
|
|
| "MEMORY_INVALIDATE"
|
|
| "WEB_STORAGE_READ"
|
|
| "WEB_STORAGE_WRITE"
|
|
| "WEB_STORAGE_REMOVE"
|
|
| "PERSISTENCE_OPEN"
|
|
| "PERSISTENCE_RESTORE"
|
|
| "PERSISTENCE_WRITE"
|
|
| "PERSISTENCE_PURGE"
|
|
| "INVALIDATION_PUBLISH"
|
|
| "INVALIDATION_RECEIVE"
|
|
| "RUNTIME_DISPOSE";
|
|
retry:
|
|
| Readonly<{ kind: "NEVER" }>
|
|
| Readonly<{ kind: "SAFE"; afterMs?: number }>
|
|
| Readonly<{ kind: "AFTER_USER_ACTION" }>;
|
|
effect: "NOT_APPLIED" | "APPLIED" | "UNKNOWN";
|
|
fallback:
|
|
| "NONE"
|
|
| "MISS"
|
|
| "MEMORY_ONLY"
|
|
| "LOCAL_ONLY"
|
|
| "ONLINE_ONLY";
|
|
}>;
|
|
```
|
|
|
|
단순 `retryable: boolean`은 retry owner, delay와 side effect certainty를 표현하지
|
|
못한다. 기존 `AppFailure`로 올려야 하는 경로는 이 closed failure를 allowlisted
|
|
kind로 mapping하되 raw native exception을 전달하지 않는다.
|
|
|
|
### 12.2 miss와 failure
|
|
|
|
다음은 reconstructable cache read에서 정상 `MISS`가 될 수 있다.
|
|
|
|
- key/record 없음
|
|
- TTL 만료
|
|
- release buster mismatch
|
|
- old account/session epoch
|
|
- 지원 정책이 `DISCARD`인 old codec
|
|
|
|
다음은 miss와 함께 degraded/security observation이 필요하다.
|
|
|
|
- corrupt envelope
|
|
- scope binding mismatch
|
|
- future/unknown version
|
|
- storage eviction
|
|
- invalidation event schema violation
|
|
|
|
unknown logical key, forbidden classification과 credential persistence 시도는
|
|
programmer/policy error다. silent miss로 숨기지 않는다.
|
|
|
|
### 12.3 effect certainty
|
|
|
|
- localStorage `setItem` return: `APPLIED`
|
|
- localStorage throw: 일반적으로 `NOT_APPLIED`, native behavior가 불명확하면
|
|
`UNKNOWN`
|
|
- IndexedDB transaction `complete`: `APPLIED`
|
|
- transaction abort: `NOT_APPLIED`
|
|
- connection loss/ambiguous lifecycle: `UNKNOWN`
|
|
- BroadcastChannel `postMessage` return: local publish accepted일 뿐 receiver effect는
|
|
`UNKNOWN`
|
|
- local Query invalidate 완료: local effect `APPLIED`, remote effect와 무관
|
|
|
|
## 13. observability와 privacy
|
|
|
|
### 13.1 event
|
|
|
|
허용 가능한 semantic event 예:
|
|
|
|
- `cache.operation.failed`
|
|
- `cache.persistence.degraded`
|
|
- `cache.scope.reset`
|
|
- `cache.invalidation.dropped`
|
|
- `cache.restore.completed`
|
|
- `storage.operation.failed`
|
|
- `storage.quota.pressure`
|
|
|
|
safe attribute:
|
|
|
|
- operation
|
|
- backend kind
|
|
- failure code
|
|
- fallback kind
|
|
- duration bucket
|
|
- byte/count bucket
|
|
- cache policy ID
|
|
- build/release compatibility epoch
|
|
|
|
금지:
|
|
|
|
- storage physical/logical key 원문
|
|
- query key, hash input와 filter
|
|
- cached value
|
|
- user/account/tenant ID
|
|
- URL, token, file name
|
|
- native error message/stack
|
|
- Broadcast event payload 원문
|
|
|
|
### 13.2 metrics
|
|
|
|
- memory cache entry/active/inactive count bucket
|
|
- hit/miss/stale/background-error ratio
|
|
- restore success/miss/busted/corrupt/deadline bucket
|
|
- persisted bytes/record count bucket
|
|
- quota/blocked/versionchange count
|
|
- invalidation publish/receive/drop/duplicate/coalesced count
|
|
- scope reset duration과 cleanup incomplete
|
|
- listener/channel/connection leak count
|
|
|
|
metric은 policy tuning의 근거지만 user data cardinality를 telemetry에 복제하지
|
|
않는다.
|
|
|
|
### 13.3 diagnostics failure
|
|
|
|
diagnostics/telemetry sink failure가 query, storage, logout 또는 dispose를
|
|
실패시키면 안 된다. observation callback은 exception을 닫고 재귀 event를 만들지
|
|
않는다.
|
|
|
|
## 14. contract test
|
|
|
|
### 14.1 TanStack memory contract
|
|
|
|
- runtime마다 독립 QueryClient
|
|
- query/mutation retry owner
|
|
- fresh/stale/inactive/gc
|
|
- namespace prefix exact invalidation
|
|
- unrelated namespace 보존
|
|
- invalidate와 remove/reset 차이
|
|
- active refetch와 inactive no-refetch
|
|
- background failure에서 stale data 유지
|
|
- query AbortSignal 전달
|
|
- scope reset cancel/detach/clear
|
|
- old scope late result가 new client에 쓰이지 않음
|
|
- File/Blob/credential query data persistence 거절
|
|
- diagnostics에 query key/value 미노출
|
|
|
|
### 14.2 Web Storage contract
|
|
|
|
- registered backend exact selection
|
|
- `HIT`/`MISS`
|
|
- local/session isolation
|
|
- schema/codec round-trip
|
|
- TTL boundary와 expired cleanup
|
|
- memory fallback도 동일 TTL/scope
|
|
- old version discard와 approved migration
|
|
- corrupt JSON/envelope/payload
|
|
- serialization cycle/BigInt/non-finite/oversize 거절
|
|
- `SecurityError`, unavailable와 quota zero
|
|
- quota cleanup 뒤 최대 1회 retry
|
|
- exact key remove, origin-wide clear 없음
|
|
- opaque partition key와 logout purge
|
|
- storage failure diagnostics redaction
|
|
|
|
### 14.3 IndexedDB query persistence contract
|
|
|
|
- fresh database/open/restore
|
|
- exact scope/policy binding
|
|
- wrong session/account/release epoch 거절
|
|
- query/payload codec와 future version
|
|
- TTL, byte/count/restore hard cap
|
|
- transaction complete 뒤 성공
|
|
- abort/close/quota/blocked/versionchange
|
|
- multi-tab concurrent writer와 CAS
|
|
- namespace epoch가 old record resurrection 차단
|
|
- hint가 commit보다 먼저 발행되지 않음
|
|
- bounded maintenance/checkpoint
|
|
- logout/account deletion exact partition purge
|
|
- optional failure가 memory-only로 degrade
|
|
- dispose connection/timer leak 0
|
|
|
|
### 14.4 cross-tab deterministic contract
|
|
|
|
- publisher local apply
|
|
- self echo drop
|
|
- duplicate event drop
|
|
- out-of-order old epoch drop
|
|
- unknown version/topic drop
|
|
- wrong scope/release drop
|
|
- expired/oversize/malformed event drop
|
|
- invalidation payload에 data/query args 없음
|
|
- BroadcastChannel primary
|
|
- localStorage pulse fallback
|
|
- sessionStorage가 fallback으로 사용되지 않음
|
|
- transport failure가 committed mutation을 실패시키지 않음
|
|
- namespace burst coalescing과 bounded queue
|
|
- `dispose()` 뒤 event 처리 없음
|
|
|
|
## 15. real browser evidence
|
|
|
|
fake와 jsdom은 native storage/context semantics를 증명하지 않는다. Chromium,
|
|
Firefox, WebKit에서 실제 browser context와 page를 사용한다.
|
|
|
|
필수 scenario:
|
|
|
|
1. 두 page에서 BroadcastChannel invalidation
|
|
2. BroadcastChannel을 제거한 환경의 localStorage pulse fallback
|
|
3. publisher에는 storage event가 오지 않아도 local invalidation 적용
|
|
4. sessionStorage가 tab별로 분리됨
|
|
5. opener snapshot을 가진 새 tab의 instance/scope 재검증
|
|
6. account switch 중 in-flight query와 late result
|
|
7. logout/reset event loss 후에도 local/session owner 정리
|
|
8. persisted record와 namespace epoch의 concurrent update
|
|
9. old tab이 IndexedDB upgrade를 막는 blocked/versionchange
|
|
10. quota/corrupt/evicted/unavailable recovery
|
|
11. pagehide/bfcache/StrictMode에서 listener/channel/connection leak 없음
|
|
12. N-1 release와 incompatible release buster
|
|
|
|
promotion artifact는 engine, browser version, OS/image, build/release ID, contract
|
|
suite version, pass/fail/skip과 실행 시각을 기록한다. fake 통과를 native provider
|
|
통과로 보고하지 않는다.
|
|
|
|
현재 source에는 native two-page BroadcastChannel delivery와 BroadcastChannel을
|
|
제외한 localStorage pulse fallback/cleanup case가 있다. 이는 transport source
|
|
contract evidence다. 다음은 아직 없다.
|
|
|
|
- production QueryClient/coordinator를 두 page에 조립한 namespace invalidation E2E
|
|
- session/account transition 중 query/mutation late-result fence
|
|
- event loss와 auth-owner local lifecycle 결합
|
|
- bfcache/StrictMode에서 production composition leak 검증
|
|
- Chromium/Firefox/WebKit 세 engine의 동일한 promotion artifact
|
|
|
|
따라서 native transport spec의 존재를 “real-browser evidence 없음”으로 축소하지
|
|
않고, 반대로 그것을 Gate 3 전체 완료로 확대하지 않는다.
|
|
|
|
## 16. optional composition
|
|
|
|
### 16.1 capability 상태
|
|
|
|
```text
|
|
InstallationState
|
|
NOT_SELECTED | INSTALLED | REMOVING
|
|
|
|
RuntimeAvailability
|
|
UNKNOWN | AVAILABLE | DEGRADED | UNAVAILABLE | INCOMPATIBLE
|
|
|
|
TrafficAdmission
|
|
DISABLED | CANARY | ENABLED
|
|
```
|
|
|
|
이 문서의 `AVAILABLE_NOT_COMPOSED`는 현재 source/runtime/contract/native test가
|
|
실제로 존재하는 generic IndexedDB reference runtime에 적용한다. source에
|
|
runtime, contract, test와 문서가 있지만:
|
|
|
|
- `InstallationState=NOT_SELECTED`
|
|
- `TrafficAdmission=DISABLED`
|
|
- production bootstrap import 없음
|
|
- database/channel/listener/timer 생성 없음
|
|
- network/refetch 부가 traffic 없음
|
|
|
|
인 상태다. query persistence facade는 아직 source runtime이 없으므로
|
|
`AVAILABLE_NOT_COMPOSED`가 아니라 `DESIGNED_NOT_IMPLEMENTED`다. 현재 cross-tab
|
|
invalidate-only runtime은 installed query contract가 실제로 소비하므로
|
|
`COMPOSED` infrastructure다.
|
|
|
|
### 16.2 기본 조립
|
|
|
|
| capability | 기본 |
|
|
| --- | --- |
|
|
| TanStack memory QueryClient | `COMPOSED` |
|
|
| 등록된 COLOR_SCHEME localStorage | `COMPOSED` |
|
|
| 등록된 CHUNK_RELOAD_GUARD sessionStorage | `COMPOSED` |
|
|
| generic IndexedDB repository/maintenance reference | `AVAILABLE_NOT_COMPOSED` |
|
|
| IndexedDB query persistence facade | `DESIGNED_NOT_IMPLEMENTED` |
|
|
| product query persistence | `NOT_SELECTED` |
|
|
| 등록 query의 cross-tab invalidation coordinator | `COMPOSED` |
|
|
| durable invalidation ledger | `DESIGNED_NOT_IMPLEMENTED` |
|
|
| SSR dehydration/hydration | `NOT_SELECTED` |
|
|
|
|
현재 installed reference feature가 query namespace/topic을 등록하므로
|
|
cross-tab coordinator와 channel/fallback을 조립한다. query persistence용 DB는
|
|
열지 않는다. 향후 installed query registry가 비게 되면 빈 channel을 열지 않는
|
|
composition으로 함께 변경해야 한다.
|
|
|
|
### 16.3 선택 절차
|
|
|
|
```text
|
|
measured product need
|
|
-> data/query owner
|
|
-> classification + authority + retention
|
|
-> query/storage registry profile
|
|
-> codec + scope/epoch
|
|
-> optional adapter composition
|
|
-> deterministic contract
|
|
-> three-engine evidence
|
|
-> disabled
|
|
-> canary
|
|
-> enabled
|
|
```
|
|
|
|
canary에서 restore latency, stale ratio, quota, drop, scope reset과 error budget을
|
|
관측한다. kill switch는 persistence write/restore 또는 cross-tab publish/listen을
|
|
독립적으로 끌 수 있어야 한다. memory cache와 정상 server fetch fallback은
|
|
유지한다.
|
|
|
|
### 16.4 capability probing
|
|
|
|
static API 존재, construction 성공, small operation 성공과 장기 availability를
|
|
구분한다.
|
|
|
|
- Web Storage property/get/set/remove small probe
|
|
- IndexedDB bounded open/write/read/delete probe
|
|
- BroadcastChannel construction/post/close local contract
|
|
- runtime scope/binding compatibility
|
|
|
|
probe는 짧은 timeout, cancellation, TTL+jitter와 concurrent dedupe를 사용한다.
|
|
매 request의 authorization/availability proof로 사용하지 않는다. 결과에는 safe
|
|
status, checked time, capability/contract version과 bounded reason만 남긴다.
|
|
|
|
## 17. 제거 가능성
|
|
|
|
### 17.1 제거 순서
|
|
|
|
1. runtime config/selection에서 신규 restore/write/publish admission을 끈다.
|
|
2. pending writer, refetch와 receiver queue를 drain/cancel한다.
|
|
3. listener, channel, timer와 DB connection을 dispose한다.
|
|
4. old release가 이해하는 cleanup-only 배포에서 owned persisted partition을 bounded
|
|
purge한다.
|
|
5. query/storage registry와 composition registration을 제거한다.
|
|
6. optional adapter, contract test와 dependency를 제거한다.
|
|
7. bundle/module inventory, SBOM과 dependency baseline을 갱신한다.
|
|
8. typecheck, architecture, base tests, build와 production artifact absence를
|
|
검증한다.
|
|
|
|
IndexedDB code부터 삭제하면 이전 browser data를 cleanup할 실행 경로가 사라질 수
|
|
있다. 필요한 retention 기간 동안 cleanup-only release를 먼저 운영한다.
|
|
|
|
### 17.2 removal gate
|
|
|
|
임시 repository copy에서 optional query persistence/cross-tab source와 관련 test,
|
|
catalog entry를 제거한 뒤 다음을 검증한다.
|
|
|
|
- base typecheck와 architecture gate 통과
|
|
- memory-only QueryClient와 Web Storage preference 동작
|
|
- 전체 base test와 production build 통과
|
|
- optional vendor/package import 부재
|
|
- Vite production module inventory에 removed source 부재
|
|
- IndexedDB DB/channel/event name과 feature flag 문자열 부재
|
|
- unselected 상태에서 zero DB open/channel/listener/network side effect
|
|
|
|
### 17.3 남겨서는 안 되는 운영 자원
|
|
|
|
- orphan IndexedDB database와 old partition
|
|
- localStorage pulse/epoch key
|
|
- BroadcastChannel/listener
|
|
- cleanup timer/worker
|
|
- alert/dashboard/runbook owner
|
|
- runtime flag와 stale config
|
|
- dependency/SBOM entry
|
|
|
|
## 18. source 배치
|
|
|
|
현재 구현:
|
|
|
|
```text
|
|
src/contracts/
|
|
cache-invalidation.ts
|
|
query-invalidation.ts
|
|
query-keys.ts
|
|
storage-keys.ts
|
|
|
|
src/adapters/query-cache/
|
|
tanstack-query-cache.ts
|
|
tanstack-cache-coordinator.ts
|
|
|
|
src/adapters/cross-context-invalidation/
|
|
browser-cross-context-invalidation.ts
|
|
browser-cross-context-host.ts
|
|
index.ts
|
|
|
|
src/adapters/storage/
|
|
browser-storage-adapter.ts
|
|
browser-storage-codec.ts
|
|
indexeddb/ # 기존 reference runtime 재사용
|
|
|
|
src/bootstrap/
|
|
runtime-adapters.ts
|
|
runtime-application.tsx
|
|
create-runtime-composition.ts
|
|
|
|
tests/unit/
|
|
query-cache.test.ts
|
|
storage-registry.test.ts
|
|
cross-tab-invalidation.test.ts
|
|
tanstack-cache-coordinator.test.ts
|
|
```
|
|
|
|
query persistence를 실제 선택할 때 추가할 경계:
|
|
|
|
```text
|
|
src/adapters/query-cache/
|
|
query-persistence-policy.ts
|
|
indexeddb-query-persistence.ts
|
|
|
|
tests/unit/
|
|
query-persistence.test.ts
|
|
|
|
tests/browser-capabilities/
|
|
client-cache-multi-tab.spec.ts
|
|
client-cache-persistence.spec.ts
|
|
```
|
|
|
|
현재 bootstrap infrastructure bundle은 concrete QueryClient, coordinator,
|
|
status와 idempotent `dispose()`를 provider tree에만 전달한다. application API에는
|
|
QueryClient, channel, native Storage 또는 IndexedDB type이 나타나지 않는다.
|
|
|
|
## 19. 구현 현황과 후속 순서
|
|
|
|
후속 구현은 VD-13 gate를 순서대로 통과한다. 뒤 gate를 먼저 구현해 앞 gate의
|
|
scope/policy를 우회하지 않는다.
|
|
|
|
### Gate 0 — 상태와 문서
|
|
|
|
- capability 상태를 이 문서의 다섯 표준 상태로만 표현
|
|
- current source, target contract와 product selection을 별도 열로 유지
|
|
- runbook/catalog/test evidence가 같은 상태와 browser case 수를 사용
|
|
|
|
현재 이 문서와 VD-13의 상태 정규화만 완료됐다. 다른 문서/catalog의 상태 변경은
|
|
실제 source/composition 변경과 함께 별도 반영한다.
|
|
|
|
### Gate 1 — strict registry, key와 Web Storage
|
|
|
|
- query policy에 classification/scope/stale/gc/retry/result budget 추가
|
|
- query key closed codec과 depth/node/part/string/byte 절대 상한 구현
|
|
- Web Storage key별 byte cap, `HIT`/`MISS`, durability outcome 구현
|
|
- partition-aware v2 envelope, adjacent migration/discard, bounded sweep 구현
|
|
- quota exact cleanup 뒤 최대 한 번 retry와 clock-skew/TTL 검증 구현
|
|
|
|
Gate 1 delta 상태: `DESIGNED_NOT_IMPLEMENTED`. 기존 closed value codec, v1 envelope,
|
|
global 16,384-byte cap과 memory overlay는 이 gate의 출발점이지 완료 증거가 아니다.
|
|
|
|
### Gate 2 — scope-owned QueryClient lifecycle
|
|
|
|
- session authority의 opaque scope snapshot과 generation 구현
|
|
- auth owner subscription에서 old admission fence 실행
|
|
- query cancel, provider detach, client clear/dispose 후 새 QueryClient 생성
|
|
- old query/mutation late result의 UI/cache update 폐기
|
|
- exact old partition logout/account lifecycle
|
|
|
|
Gate 2 delta 상태: `DESIGNED_NOT_IMPLEMENTED`. 현재 `resetLocal()`은 coordinator-local
|
|
cancel/clear만 제공하며 session/account transition에 bootstrap 조립되지 않았다.
|
|
|
|
### Gate 3 — cross-tab scope hardening
|
|
|
|
- release-only epoch를 composite session/account/release fingerprint로 교체
|
|
- localStorage pulse key registry 등록과 exact `storageArea` 검증
|
|
- topic/profile count absolute cap
|
|
- production QueryClient/coordinator two-page E2E
|
|
- account switch, event loss, bfcache/StrictMode cleanup
|
|
- Chromium/Firefox/WebKit 동일 promotion artifact
|
|
|
|
현재 transport/coordinator는 `COMPOSED`이고 native BroadcastChannel/localStorage
|
|
fallback 두 case도 존재한다. 위 scope/account lifecycle과 세 engine artifact가
|
|
없으므로 Gate 3은 완료되지 않았다.
|
|
|
|
### Gate 4 — optional query persistence reference
|
|
|
|
- stable per-query record codec와 generic IndexedDB runtime facade
|
|
- durable namespace epoch, CAS와 commit-before-hint
|
|
- bounded restore/write/debounce/dispose
|
|
- scope/release/TTL/codec/quota/blocked/migration failure
|
|
- unselected production bundle에서 zero DB open/listener/network side effect
|
|
- removal/module-inventory gate
|
|
|
|
현재 reference facade와 ledger는 `DESIGNED_NOT_IMPLEMENTED`다. 구현과 test가
|
|
완료돼도 product가 선택하기 전 상태는 reference
|
|
`AVAILABLE_NOT_COMPOSED`, product `NOT_SELECTED`다.
|
|
|
|
### Gate 5 — product composition
|
|
|
|
- measured warm-start/offline requirement와 owner 승인
|
|
- exact persist allowlist, retention/budget/account/logout policy
|
|
- disabled → canary → enabled admission
|
|
- N-1 reader/writer, rollback과 cleanup-only release drill
|
|
|
|
현재 상태: `NOT_SELECTED`.
|
|
|
|
### Gate 6 — SSR/offline workflow
|
|
|
|
SSR과 offline mutation은 각각 독립 capability다.
|
|
|
|
- SSR 선택 시 request-scoped QueryClient, safe dehydration, SSR 우선 merge와
|
|
request isolation을 구현
|
|
- offline mutation 선택 시 feature-specific repository, backend
|
|
idempotency/revision/sync/conflict와 export/recovery UX를 구현
|
|
|
|
현재 두 product capability 모두 `NOT_SELECTED`다. query persistence 구현이 이
|
|
gate를 자동 충족하지 않는다.
|
|
|
|
## 20. 완료 기준
|
|
|
|
- [x] TanStack cache, Web Storage, IndexedDB와 invalidation bus의 소유권이
|
|
source/module 경계에서 분리됐다.
|
|
- [x] `QUERY_REGISTRY`, coordinator와 `QUERY_PERSISTENCE` key가 query persistence를
|
|
기본 OFF로 강제한다.
|
|
- [ ] 승인 profile 전용 IndexedDB query persistence facade가 구현됐다.
|
|
- [x] 등록 Web Storage key가 closed value codec, schema version, TTL 선언과
|
|
16,384-byte adapter hard cap을 갖는다.
|
|
- [ ] 등록 Web Storage key마다 byte cap, `HIT`/`MISS`, durability, partition,
|
|
logout, migration과 bounded sweep 계약이 실행 코드로 강제된다.
|
|
- [x] credential key는 persistence가 금지되고 invalidation event는 exact
|
|
payload/query-key-free envelope만 허용한다.
|
|
- [ ] query registry가 classification/scope/stale/gc/retry/result budget을
|
|
검증하고 strict key codec이 hostile/native/oversize input을 거절한다.
|
|
- [ ] session/account epoch와 opaque partition이 memory/persistence/event에
|
|
binding된다. 현재는 release cache epoch만 있다.
|
|
- [x] remote protocol은 invalidate-only이고 remove/reset/clear는 local
|
|
coordinator authority에만 있다.
|
|
- [ ] local remove/reset/clear와 account lifecycle 전체가 contract/real-browser
|
|
test로 고정됐다.
|
|
- [ ] IndexedDB query persistence transaction complete 뒤에만 hint를 발행한다.
|
|
현재 query persistence 자체가 OFF다.
|
|
- [x] deterministic test에서 duplicate, sequence gap, stale out-of-order와
|
|
self-echo가 안전하다.
|
|
- [x] BroadcastChannel open/publish 실패에서 localStorage pulse 또는
|
|
`DEGRADED_LOCAL_ONLY`로 전환한다.
|
|
- [ ] localStorage pulse가 registry-owned key와 exact `storageArea`를 검증한다.
|
|
- [x] installed query registry와 coordinator가 production bootstrap/provider
|
|
tree에 조립됐다.
|
|
- [ ] logout/account switch와 late result fence가 broadcast delivery에 의존하지
|
|
않고 동작한다.
|
|
- [ ] SSR을 제품이 선택한 경우 request isolation, safe dehydration과 hydration
|
|
precedence가 실행 코드와 test로 구현됐다. 현재 SSR은 `NOT_SELECTED`다.
|
|
- [x] Web Storage quota/corrupt/oversize/unavailable failure와 redaction이 닫혀
|
|
있다.
|
|
- [ ] IndexedDB query restore의 blocked/deadline/quota/corrupt failure가 닫혀
|
|
있다.
|
|
- [x] 현재 invalidation listener/channel/tracking state에 idempotent
|
|
unsubscribe/close/dispose가 있다.
|
|
- [x] invalidation diagnostics에는 event/topic/epoch/source/value/query key가
|
|
노출되지 않고 storage diagnostics에는 physical key/value가 노출되지 않는다.
|
|
- [x] native two-page BroadcastChannel과 localStorage fallback source spec이 있다.
|
|
- [ ] Chromium/Firefox/WebKit의 production coordinator, account transition과
|
|
lifecycle multi-tab promotion evidence가 있다.
|
|
- [ ] cross-tab/query-persistence 전용 removal/zero-side-effect gate가 통과한다.
|
|
|
|
## 21. 관련 자료
|
|
|
|
- [API contract, Schema, Mapper와 Server State](./api-contract-schema-mapper-and-server-state.md)
|
|
- [VD-25 Server State Cache lifecycle](./decisions/VD-25-server-state-cache-lifecycle.md)
|
|
- [Browser data capability completion ledger](./browser-data-capability-completion-ledger.md)
|
|
- [TypeScript, 상태 소유권, 데이터 흐름](./typescript-state-and-data-flow.md)
|
|
- [Frontend ports, adapters and boundaries](./frontend-ports-adapters-and-boundaries.md)
|
|
- [Browser file and origin-storage platform](./browser-file-and-origin-storage.md)
|
|
- [VD-11 browser file and origin-storage 경계](./decisions/VD-11-browser-file-and-origin-storage.md)
|
|
- [VD-13 client cache scope와 persistence](./decisions/VD-13-client-cache-scope-and-persistence.md)
|
|
- [Client cache and Web Storage recovery](../operations/client-cache-and-storage-recovery.md)
|
|
- [TanStack Query important defaults](https://tanstack.com/query/latest/docs/framework/react/guides/important-defaults)
|
|
- [TanStack Query persistence](https://tanstack.com/query/v5/docs/framework/react/plugins/persistQueryClient)
|
|
- [TanStack experimental broadcast client](https://tanstack.com/query/latest/docs/framework/react/plugins/broadcastQueryClient)
|
|
- [MDN Web Storage API](https://developer.mozilla.org/docs/Web/API/Web_Storage_API)
|
|
- [MDN storage event](https://developer.mozilla.org/docs/Web/API/Window/storage_event)
|
|
- [MDN sessionStorage](https://developer.mozilla.org/docs/Web/API/Window/sessionStorage)
|
|
- [MDN Broadcast Channel API](https://developer.mozilla.org/docs/Web/API/Broadcast_Channel_API)
|
|
- [MDN IndexedDB](https://developer.mozilla.org/docs/Web/API/IndexedDB_API/Using_IndexedDB)
|
|
|
|
Web platform과 TanStack library는 계속 변한다. 실제 조립 시 pinned dependency와
|
|
지원 browser matrix의 공식 문서를 다시 검증하고 contract evidence에 version을
|
|
기록한다.
|