Files
tech-log-frontend/docs/architecture/client-cache-and-storage.md
T

71 KiB

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 저장소 정합형 구현 결정 폐쇄 상세 설계가 정본이다. 본 문서의 해당 서술이 정본과 충돌하면 정본을 따른다.

  • 상태: capability별 current/target 상태 분리
  • 기준일: 2026-07-28
  • 범위: TanStack Query memory cache, Local/Session Storage, IndexedDB query persistence 경계, 탭 간 invalidation
  • 관련 결정: VD-13 client cache scope와 persistence
  • 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.tssrc/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.tsbrowser-storage-adapter.ts는 closed JSON codec, 기본 16,384-byte 상한, schema envelope, TTL과 동일 envelope를 쓰는 memory fallback을 제공한다. StorageDefinition.valueCodeccolor-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의 migrationdiscard만 허용하며 실제 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.tssrc/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.tsxQueryInvalidationProvider를 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.tssrc/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로 합치지 않는다

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
  • localStoragesessionStorage의 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

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을 가진다.

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의 권장 순서는 다음과 같다.

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에 묶는다.

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-25ServerStateProfile이 유일하게 소유한다.

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:

[
  "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.tscanonicalize()는 object key order는 정규화하지만 cycle과 non-serializable input을 닫지 않는다. production registry는 이를 검증하는 codec을 추가해야 한다.

4.4 Web Storage registry

각 logical key는 최소 다음을 갖는다.

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:

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 }만 반환하지 않는다.

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_PERSISTENCEdisabledsensitive-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:

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 scopeFingerprintreleaseEpoch를 검증한다. 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 종류

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

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

기본 우선순위:

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

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

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 상태

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 선택 절차

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 배치

현재 구현:

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를 실제 선택할 때 추가할 경계:

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. 완료 기준

  • TanStack cache, Web Storage, IndexedDB와 invalidation bus의 소유권이 source/module 경계에서 분리됐다.
  • QUERY_REGISTRY, coordinator와 QUERY_PERSISTENCE key가 query persistence를 기본 OFF로 강제한다.
  • 승인 profile 전용 IndexedDB query persistence facade가 구현됐다.
  • 등록 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 계약이 실행 코드로 강제된다.
  • 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만 있다.
  • 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다.
  • deterministic test에서 duplicate, sequence gap, stale out-of-order와 self-echo가 안전하다.
  • BroadcastChannel open/publish 실패에서 localStorage pulse 또는 DEGRADED_LOCAL_ONLY로 전환한다.
  • localStorage pulse가 registry-owned key와 exact storageArea를 검증한다.
  • 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다.
  • Web Storage quota/corrupt/oversize/unavailable failure와 redaction이 닫혀 있다.
  • IndexedDB query restore의 blocked/deadline/quota/corrupt failure가 닫혀 있다.
  • 현재 invalidation listener/channel/tracking state에 idempotent unsubscribe/close/dispose가 있다.
  • invalidation diagnostics에는 event/topic/epoch/source/value/query key가 노출되지 않고 storage diagnostics에는 physical key/value가 노출되지 않는다.
  • 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. 관련 자료

Web platform과 TanStack library는 계속 변한다. 실제 조립 시 pinned dependency와 지원 browser matrix의 공식 문서를 다시 검증하고 contract evidence에 version을 기록한다.