feat: 기능 추가 과정중
This commit is contained in:
@@ -36,10 +36,20 @@ JS/JSX/TS/TSX가 같은 품질 게이트를 통과하게 해야 한다.
|
||||
6. Babel 8의 지원 범위에 맞춰 Node engine 하한을 `24.11.0`으로 명시한다.
|
||||
7. production source의 대량 rename은 이 결정에 포함하지 않는다.
|
||||
|
||||
## 적용 후 상태 (2026-07-27)
|
||||
|
||||
후속 migration에서 production source, 비-fixture tests, Node scripts와 지원되는
|
||||
tool config를 모두 TS/TSX로 전환했다. `allowJs`는 껐고 runtime/source 영역의
|
||||
JavaScript 재유입은 architecture gate가 거절한다. Node scripts는 pinned Node
|
||||
24에서 `.ts`로 직접 실행되며 NodeNext, `verbatimModuleSyntax`와
|
||||
`erasableSyntaxOnly`로 별도 typecheck한다. `tests/fixtures/**`도 TS/TSX
|
||||
architecture/security/type negative input으로 전환했다. 이는 7번 결정의 범위를
|
||||
변경한 것이 아니라 그 기반 위에서 완료한 별도 후속 작업이다.
|
||||
|
||||
## 검증
|
||||
|
||||
- `check:types`는 app, Node scripts/config, tests project를 모두 검사한다.
|
||||
- JS invalid-call, TS invalid port, TS discriminated-union fixture는 실패해야 한다.
|
||||
- TS invalid-call, invalid port, discriminated-union fixture는 실패해야 한다.
|
||||
- ESLint와 dependency-cruiser는 TS/TSX architecture fixture를 검사한다.
|
||||
- registry scanner는 TS registry의 required field, uniqueness와 reference를
|
||||
검증한다.
|
||||
|
||||
@@ -57,8 +57,8 @@ route/form/failure 의미 값
|
||||
```
|
||||
|
||||
- canonical catalog: `src/presentation/i18n/catalog.ts`
|
||||
- feature contribution: `src/features/*/contracts/*-message-catalog.js`를
|
||||
`src/features/installed-feature-messages.js`에서 조립
|
||||
- feature contribution: `src/features/*/contracts/*-message-catalog.ts`를
|
||||
`src/features/installed-feature-messages.ts`에서 조립
|
||||
- key/보간/fallback/alias: `message-contract.ts`
|
||||
- locale-safe value formatting: `formatters.ts`
|
||||
- React composition과 document metadata: `locale-provider.tsx`
|
||||
|
||||
@@ -62,12 +62,12 @@ route/application/HTTP/cache/storage/bootstrap
|
||||
```
|
||||
|
||||
- diagnostics contract: `src/contracts/diagnostics.ts`
|
||||
- telemetry contract: `src/contracts/telemetry.js`
|
||||
- telemetry contract: `src/contracts/telemetry.ts`
|
||||
- application ports: `src/application/ports/diagnostics-port.ts`,
|
||||
`telemetry-port.ts`
|
||||
- bounded diagnostics: `src/adapters/diagnostics/bounded-diagnostics.ts`
|
||||
- best-effort telemetry: `src/adapters/telemetry/best-effort-telemetry.js`
|
||||
- composition: `src/bootstrap/runtime-adapters.js`
|
||||
- best-effort telemetry: `src/adapters/telemetry/best-effort-telemetry.ts`
|
||||
- composition: `src/bootstrap/runtime-adapters.ts`
|
||||
|
||||
## 검증
|
||||
|
||||
|
||||
@@ -50,9 +50,9 @@ application bundle에 workshop runtime을 포함하는 것 모두 적절하지
|
||||
- interaction/a11y: `tests/storybook/workshop.spec.ts`
|
||||
- visual: `tests/visual/platform.visual.spec.ts`
|
||||
- baseline: `tests/visual/__snapshots__/`
|
||||
- production E2E: `playwright.config.js`
|
||||
- local dev E2E: `playwright.dev.config.js`
|
||||
- evidence policy: `scripts/check-test-evidence.mjs`
|
||||
- production E2E: `playwright.config.ts`
|
||||
- local dev E2E: `playwright.dev.config.ts`
|
||||
- evidence policy: `scripts/check-test-evidence.ts`
|
||||
|
||||
CI는 JUnit, HTML report, failure trace/screenshot, visual baseline 존재 여부와
|
||||
금지된 full-screen mask/무소유 skip fixture를 함께 검사한다.
|
||||
|
||||
@@ -66,10 +66,10 @@ source/config/lock + production dist
|
||||
```
|
||||
|
||||
- policy: `config/security/`
|
||||
- generator: `scripts/generate-supply-chain.mjs`
|
||||
- coherence: `scripts/verify-supply-chain-artifacts.mjs`
|
||||
- secret scan: `scripts/security-scan.mjs`
|
||||
- reproducibility: `scripts/verify-reproducible-build.mjs`
|
||||
- generator: `scripts/generate-supply-chain.ts`
|
||||
- coherence: `scripts/verify-supply-chain-artifacts.ts`
|
||||
- secret scan: `scripts/security-scan.ts`
|
||||
- reproducibility: `scripts/verify-reproducible-build.ts`
|
||||
- inventory: `artifacts/release/dependency-inventory.json`
|
||||
- SBOM/provenance: `artifacts/release/sbom.cdx.json`,
|
||||
`artifacts/release/provenance.json`
|
||||
|
||||
@@ -48,8 +48,15 @@ fallback, fake와 제거 기준을 다시 설계해야 한다. 따라서 product
|
||||
접속하는 recipe도 금지한다.
|
||||
8. lifecycle이 있는 capability는 unsubscribe, close, unregister, dispose,
|
||||
cancel 또는 `AbortSignal`을 계약과 contract test에 포함해야 한다.
|
||||
9. 선택하지 않은 recipe sentinel이나 vendor dependency가 production bundle에
|
||||
들어가면 gate를 실패시킨다.
|
||||
9. 선택하지 않은 recipe sentinel이나 reference runtime source, vendor
|
||||
dependency가 production bundle에 들어가면 gate를 실패시킨다.
|
||||
`referenceRuntime`이 있는 recipe는 catalog `sourceRoots` 전체를 별도의
|
||||
production-mode synthetic entry로 deterministic하게 bundle/minify하되
|
||||
tree-shaking을 끄고, 모든 출력의 gzip 합계가 recipe budget을 넘으면
|
||||
production composition 여부와 무관하게 실패시킨다. 2026-07-28 최초 실측에서
|
||||
`offline-indexeddb`가 32,930 bytes였으므로 측정 없이 선언됐던 8,000 bytes를
|
||||
약 9% headroom의 36,000 bytes로 교정했으며 다른 budget은 자동 인상하지
|
||||
않는다.
|
||||
10. recipe 전체를 제거한 임시 worktree에서 base typecheck, architecture,
|
||||
unit/component/integration test와 production build가 통과해야 한다.
|
||||
|
||||
@@ -77,13 +84,20 @@ behavior를 fake만으로 확인하고 `INSTALLED`로 바꾸지 않는다.
|
||||
- catalog: `config/recipes/frontend-capability-recipes.json`
|
||||
- contracts/fakes: `recipes/frontend-capabilities`
|
||||
- 상세 runbook: `docs/architecture/optional-adapter-recipes.md`
|
||||
- file/IndexedDB/OPFS/Cache 심층 결정:
|
||||
`docs/architecture/decisions/VD-11-browser-file-and-origin-storage.md`
|
||||
- browser data 상세 설계:
|
||||
`docs/architecture/browser-file-and-origin-storage.md`
|
||||
- realtime/Web Push/Polling 심층 설계와 결정:
|
||||
`docs/architecture/realtime-events-web-push-and-bounded-polling.md`,
|
||||
`docs/architecture/decisions/VD-28-realtime-events-web-push-and-bounded-polling.md`
|
||||
- contract test: `tests/recipes/optional-capability-contracts.test.ts`
|
||||
- negative fixture:
|
||||
`tests/fixtures/optional-recipes/forbidden`
|
||||
- validation:
|
||||
`scripts/check-optional-recipes.mjs`
|
||||
`scripts/check-optional-recipes.ts`
|
||||
- removal:
|
||||
`scripts/test-optional-recipe-removal.mjs`
|
||||
`scripts/test-optional-recipe-removal.ts`
|
||||
- evidence:
|
||||
`artifacts/quality/optional-recipes.json`,
|
||||
`artifacts/quality/optional-recipe-fixtures.json`,
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
# VD-11: Browser file and origin-storage 경계
|
||||
|
||||
- 상태: Accepted — native reference runtime available, not composed
|
||||
- 결정일: 2026-07-27
|
||||
- reference runtime 상태: `AVAILABLE_NOT_COMPOSED`
|
||||
- catalog recipe availability: `RECIPE_AVAILABLE` (primary status/selection과 별도)
|
||||
- 관련 결정: VD-10 optional capability recipes, VD-14, VD-15
|
||||
- current status ledger:
|
||||
`docs/architecture/browser-data-capability-completion-ledger.md`
|
||||
- 재검토: 실제 제품이 file intake/delivery, durable offline data, large local
|
||||
binary 또는 public offline HTTP representation을 선택할 때
|
||||
|
||||
## 배경
|
||||
|
||||
File, Blob, picker, IndexedDB, OPFS와 Cache Storage는 모두 browser data를
|
||||
다루지만 같은 storage abstraction이 아니다.
|
||||
|
||||
- File/Blob은 transient byte container다.
|
||||
- picker는 user activation과 permission UX를 소유한다.
|
||||
- IndexedDB는 indexed structured record와 transaction을 제공한다.
|
||||
- OPFS는 origin-private large byte storage지만 query와 cross-API transaction이
|
||||
없다.
|
||||
- Cache Storage는 HTTP Request/Response map이며 freshness를 자동 관리하지 않는다.
|
||||
|
||||
이를 하나의 `StoragePort`나 `FileTransferPort`로 추상화하면 transaction complete,
|
||||
blocked/versionchange, object URL 수명, stream backpressure, quota, OPFS partial
|
||||
write, Cache의 인증 response 금지와 release activation이 사라진다.
|
||||
|
||||
기존 recipe는 metadata-only upload와 in-memory `Uint8Array` download를 보여 주는
|
||||
얕은 예시였다. 큰 파일과 production recovery protocol의 출발점으로는 부족했다.
|
||||
backend upload protocol을 browser file mechanism에 묶는 것 역시 선택하지 않은
|
||||
제품 capability를 암묵적으로 설치하므로 경계를 분리해야 한다.
|
||||
|
||||
## 결정
|
||||
|
||||
1. 기존 동기식 `StoragePort`는 작은 public preference만 소유한다. IndexedDB,
|
||||
OPFS, Cache Storage를 backend enum 하나로 끼우지 않는다.
|
||||
2. native `File`, `Blob`, `FileList`, `FileSystemHandle`은 browser adapter의
|
||||
transient vault 안에 둔다. application은 opaque `LocalFileRef`, normalized
|
||||
metadata와 bounded `readRange()`만 본다.
|
||||
3. browser 파일 기능을 picker, file content, preview lease, download delivery로
|
||||
분리한다. backend upload는 `BrowserFileComposition`의 구성요소가 아니며,
|
||||
별도 선택 가능한 `ExampleQuarantinedUploadPort` 예시로만 둔다. user
|
||||
dismissal은 failure가 아닌 outcome이다.
|
||||
4. backend upload를 선택한 경우 file validation, authorization,
|
||||
malware/archive/active-content 검사와 quarantine은 client hint보다 항상
|
||||
authoritative하다.
|
||||
5. 큰 file/download/object는 stream 또는 bounded part로 처리한다. 전체
|
||||
`Uint8Array`, Blob, base64/Data URL은 승인된 hard cap 안의 small artifact에만
|
||||
사용한다. File/OPFS/Cache 및 recipe byte source는
|
||||
`AsyncIterable<Result<Uint8Array, ClosedFailure>>`로 실패를 닫고 raw native
|
||||
exception을 application으로 throw하지 않는다.
|
||||
6. download outcome은 browser handoff와 confirmed saved를 분리한다. anchor click을
|
||||
disk write 완료로 기록하지 않는다.
|
||||
7. IndexedDB는 feature-specific async repository adapter다. raw database,
|
||||
transaction callback, store/index/schema version을 application에 노출하지 않는다.
|
||||
8. DB DDL version과 record codec version을 분리한다. schema upgrade는 짧고
|
||||
additive하게, data migration은 resumable bounded batch로 수행한다.
|
||||
9. IndexedDB mutation은 request success가 아니라 transaction complete 이후에만
|
||||
성공이다. revision CAS와 idempotency key를 기본 계약으로 둔다.
|
||||
10. 모든 connection은 versionchange/forced-close를 처리하고 blocked/future-schema
|
||||
상태를 read-only 또는 online-only UX로 드러낸다. 자동 reload loop와 자동
|
||||
database deletion을 금지한다.
|
||||
11. OPFS는 큰 immutable bytes와 integrity manifest만 소유한다. logical metadata,
|
||||
query, generation과 journal commit authority는 IndexedDB가 소유한다.
|
||||
12. IndexedDB와 OPFS 사이의 비원자성은
|
||||
`PREPARING -> FILES_READY -> COMMITTED -> CLEANED` journal saga와 startup
|
||||
reconciliation으로 처리한다. `COMMITTED`만 사용자에게 보인다.
|
||||
13. OPFS sync access handle은 DedicatedWorker의 신규 staging/chunk file에만
|
||||
사용하고 항상 flush/close한다. committed file in-place overwrite와
|
||||
`readwrite-unsafe`를 금지한다.
|
||||
14. Cache Storage는 same-origin public GET representation 전용 platform-local
|
||||
facade다. auth, cookie-dependent, private, personal, no-store, opaque, 206,
|
||||
redirect response를 저장하지 않는다.
|
||||
15. Cache match는 query/Vary를 보존하고 `ignoreSearch`/`ignoreVary`를 금지한다.
|
||||
candidate 전체를 type/size/integrity 검증한 뒤에만 release를 활성화하며
|
||||
verified previous release를 rollback용으로 유지한다.
|
||||
16. Service Worker lifecycle과 Cache Storage ownership을 구분한다. unregister가
|
||||
cache 삭제를 의미하지 않으므로 owned-prefix cleanup migration을 별도로 둔다.
|
||||
17. IndexedDB, OPFS와 Cache Storage는 origin quota budget을 공유한다.
|
||||
`estimate()`는 rough signal이고 실제 `QuotaExceededError`를 authority로 둔다.
|
||||
18. credential 저장을 금지한다. same-origin client encryption을 XSS authorization
|
||||
boundary로 간주하지 않는다.
|
||||
19. fake는 계약 검증용이고 native production evidence를 대체하지 않는다.
|
||||
Chromium/Firefox/WebKit, multi-page, crash/fault, migration/rollback과 quota
|
||||
drill을 설치 capability의 promotion gate로 둔다.
|
||||
20. 공통 native adapter는 정책 주입형 reference runtime으로 제공하되 현재 제품
|
||||
owner와 dataset이 없으므로 bootstrap, installed feature, Service Worker
|
||||
registration과 runtime config에는 연결하지 않는다. catalog recipe
|
||||
availability는 `RECIPE_AVAILABLE`, reference runtime primary status는
|
||||
`AVAILABLE_NOT_COMPOSED`이며 product selection은 별도다.
|
||||
21. API lifecycle, transaction, bounded-memory, integrity와 recovery mechanism은
|
||||
공통 adapter가 소유한다. schema/codec/query, authority, classification,
|
||||
retention, quota priority와 cache/file allowlist는 dataset/use-case 정책으로
|
||||
주입한다.
|
||||
22. composition은 dataset별 opaque scope와 전체 storage policy를 검증해 깊은
|
||||
snapshot/freeze한다. 공통 runtime을 여러 dataset의 전역 mega-repository로
|
||||
구성하지 않는다.
|
||||
23. IndexedDB physical DB명은 registry-issued
|
||||
`authorityToken/namespaceToken/partitionToken`에서만 파생한다. readable
|
||||
namespace/business/account ID는 이름에 쓰지 않는다. immutable scope + full
|
||||
policy binding을 upgrade transaction, post-open과 maintenance에서 검증하고
|
||||
mismatch 또는 기존 DB의 missing binding은 fail-closed한다.
|
||||
24. IndexedDB는 codec `measureStoredBytes`, retention sidecar, dataset
|
||||
`usedBytes/receiptCount` budget을 mutation과 같은 transaction에서 갱신한다.
|
||||
TTL은 sweep 전에도 read/query에서 보이지 않으며 `UNTIL_SYNCED`는 confirmed
|
||||
record만 삭제 가능하다. lifecycle deletion은 composition authority의 opaque
|
||||
short-lived proof가 매 invocation 필요하고 proof는 검증 후 폐기한다.
|
||||
25. idempotency receipt retention은 최대 31일, receipt configured cap의 구현 절대
|
||||
상한은 1,000,000개다. codec migration은 old-writer drain proof와 revision
|
||||
fence가 필요하고 한 invocation은 최대 500 rows/30,000ms다.
|
||||
26. OPFS physical layout은
|
||||
`/ca-frontend-opfs-v1/authorities/<authority>/<namespace>/<partition>/...`이며
|
||||
세 path segment는 opaque token이다. IndexedDB journal은 logical namespace와
|
||||
physical scope를 양방향 binding하고 full policy fingerprint를 검증한다.
|
||||
27. Cache manifest는 정규화된 `expectedContentType`까지 digest에 binding한다.
|
||||
response의 정규화된 Content-Type이 정확히 일치하지 않으면 candidate activation을
|
||||
금지한다.
|
||||
28. optional 상태는 metadata만으로 주장하지 않는다. real-browser JUnit verifier,
|
||||
Vite source-module inventory, source boundary gate와 runtime removal gate를
|
||||
promotion evidence로 둔다.
|
||||
29. File selection/inspection/preview/download dataset policy는 composition-time
|
||||
registry가 소유한다. port caller는 정확히 등록된 `FilePolicyReference` 객체와
|
||||
limit reduction만 전달하며 같은 key/intention 문자열로 reference를 재구성해
|
||||
다른 profile을 선택할 수 없다. verification receipt는 exact profile과 file
|
||||
snapshot에 binding한다.
|
||||
30. `BROWSER_MANAGED_RESOURCE` download는 resource와 함께 서버 발급 capability
|
||||
receipt를 요구한다. synchronous resolver의 결과가 receipt/resource/media
|
||||
type/safe extension/server max/optional digest/expiry를 정확히 binding하지
|
||||
않으면 handoff하지 않는다. strategy와 integrity mode를 caller가 선택하지
|
||||
않는다.
|
||||
31. IndexedDB actual `StoredRecord`는
|
||||
`key/codecVersion/revision/payload`만 가지며 write time, synchronization,
|
||||
measured bytes와 eligibility는 retention sidecar에 분리한다. idempotency
|
||||
receipt와 governance binding/budget도 별도 store에 두고, full partition
|
||||
purge에는 등록된 모든 `lifecycleMetadataStores`를 포함하되 immutable
|
||||
governance identity는 유지한다. Cache cleanup retain set은 caller가 cache
|
||||
name이나 release registry ID로 제출하지 않고 verified active pointer와
|
||||
composition retention에서 계산한다. control JSON은 정확히 2 MiB
|
||||
(2,097,152 bytes) bounded stream으로만 decode한다.
|
||||
32. OPFS의 `LOGOUT`, `UNTIL_SYNCED`, `ACCOUNT_DELETION` policy maintenance는
|
||||
composition이 `requestMaintenanceAuthority` provider와
|
||||
`consumeMaintenanceAuthority` consumer를 모두 공급해야 한다. provider는
|
||||
exact reason/frozen scope/frozen policy에 묶인 최대 5분 proof를 매번 새로
|
||||
발급하고, consumer는 같은 binding과 expiry를 확인해 원자적으로 consume하여
|
||||
replay를 막는다. application caller는 proof를 전달할 수 없고 runtime은 이를
|
||||
저장·반환·관측하지 않는다.
|
||||
33. origin-wide pressure/write admission/GC, OPFS·Cache forward migration,
|
||||
OPFS real preflight, bounded Cache maintenance와 preview decode safety의
|
||||
후속 계약은 VD-15가 소유한다. 기존 store별 primitive를 그 coordinator의
|
||||
구현 증거로 사용하지 않는다.
|
||||
34. Service Worker lifecycle, directory/persistent handle과 private/sparse Range
|
||||
cache는 제품 선택 전 `NOT_SELECTED`인 별도 capability다. Range resumable
|
||||
download는 VD-14의 `DESIGNED_NOT_IMPLEMENTED` capability이며 public Cache
|
||||
runtime에 섞지 않는다.
|
||||
|
||||
## 계약과 증적
|
||||
|
||||
- 심층 계약:
|
||||
`recipes/frontend-capabilities/browser-file-storage-contracts.ts`
|
||||
- deterministic fake:
|
||||
`recipes/frontend-capabilities/browser-file-storage-fakes.ts`
|
||||
- contract test:
|
||||
`tests/recipes/browser-file-storage-contracts.test.ts`
|
||||
- selection SSOT:
|
||||
`config/recipes/frontend-capability-recipes.json`
|
||||
- 상세 설계:
|
||||
`docs/architecture/browser-file-and-origin-storage.md`
|
||||
- lifecycle/migration 결정:
|
||||
`docs/architecture/decisions/VD-15-origin-storage-lifecycle-and-migration.md`
|
||||
- 운영 복구:
|
||||
`docs/operations/browser-file-storage-recovery.md`
|
||||
- native reference runtime:
|
||||
`src/adapters/browser-files/`, `src/adapters/storage/indexeddb/`,
|
||||
`src/adapters/storage/opfs/`, `src/adapters/cache-storage/`
|
||||
- real-browser conformance:
|
||||
`tests/browser-capabilities/`
|
||||
- browser evidence verifier:
|
||||
`scripts/verify-browser-capability-evidence.ts`
|
||||
- production module inventory:
|
||||
`artifacts/quality/vite-module-inventory.json`
|
||||
- boundary/removal evidence:
|
||||
`check:browser-file-storage-boundaries`,
|
||||
`test:browser-file-storage-removal`
|
||||
|
||||
recipe의 durable byte source도 단일 `Uint8Array` 또는 raw-throw stream 대신
|
||||
chunk별 `CapabilityResult<Uint8Array>`를 반환한다. backend upload example은
|
||||
`ExampleBackendUploadComposition`으로 browser file composition과 분리되어 있다.
|
||||
실제 upload feature는 이 예시를 그대로 import하지 않고 purpose와 backend
|
||||
protocol에 맞게 contract를 더 좁힌다.
|
||||
|
||||
현재 checkout의 browser source suite는 engine마다 같은 14개 case(File 2,
|
||||
IndexedDB 4, OPFS/Cache/StorageManager 각 1, cross-context invalidation 2,
|
||||
presigned streaming download/multipart upload/Image CDN 각 1)를 정의한다.
|
||||
promotion artifact는 Chromium/Firefox/WebKit 각각 14개, 총 42개를 모두
|
||||
실행해야 한다. WebKit은 현재
|
||||
host의 필수 native libraries(예:
|
||||
`libbacktrace.so.0`, `libevent-2.1.so.7`, `libjxl.so.0.8`,
|
||||
`libavif.so.16`과 WPE 계열) 부재로 실행되지 않았다. 보존 artifact는
|
||||
Chromium/Firefox 14개씩 총 28개만 통과했으므로
|
||||
`verify:browser-capability-evidence`가 실패하는 것이 정상이다. 세 engine
|
||||
evidence가 완성되기 전에는 product 상태를 `INSTALLED`로 올리지 않는다.
|
||||
|
||||
## 선택 이후 필요한 구현
|
||||
|
||||
```text
|
||||
dataset + owner + classification + backend protocol
|
||||
-> VD-11 amendment
|
||||
-> feature-specific application ports
|
||||
-> adapter-private schema/codec/migrations
|
||||
-> native picker/file/download/IDB/OPFS/cache adapter 중 필요한 것만
|
||||
-> unavailable/read-only/online-only fallback
|
||||
-> deterministic fault + real browser contract tests
|
||||
-> diagnostics allowlist + recovery runbook drill
|
||||
-> canary + N-1 rollback evidence
|
||||
-> project catalog에서만 INSTALLED
|
||||
```
|
||||
|
||||
OPFS를 쓴다는 이유로 Service Worker를 설치하거나, Cache Storage를 쓴다는 이유로
|
||||
IndexedDB business repository를 만들지 않는다. 실제 capability 조합만 설치한다.
|
||||
|
||||
## 결과
|
||||
|
||||
장점:
|
||||
|
||||
- native API와 clean architecture 경계가 명확하다.
|
||||
- 대용량 memory blow-up과 거짓 download-complete 신호를 막는다.
|
||||
- IndexedDB migration/transaction과 OPFS crash recovery가 검증 가능하다.
|
||||
- auth/private cache poisoning을 fail-closed한다.
|
||||
- 기술별 fallback, kill switch와 제거 범위가 독립적이다.
|
||||
|
||||
비용:
|
||||
|
||||
- 하나의 generic adapter보다 port와 contract test 수가 많다.
|
||||
- native adapter 설치 시 worker, historical schema fixture, multi-page test와
|
||||
운영 drill이 필요하다.
|
||||
- offline user-authored data는 browser storage만으로 backup을 보장할 수 없어
|
||||
server sync 또는 export 제품 결정이 필요하다.
|
||||
|
||||
이 비용은 browser persistence의 실제 일관성·수명 차이를 숨기지 않기 위한
|
||||
의도적인 비용이다.
|
||||
|
||||
## Rollback
|
||||
|
||||
현재는 native reference runtime source가 있지만 production composition은 없다.
|
||||
catalog의 세 runtime은 `AVAILABLE_NOT_COMPOSED` /
|
||||
`productionComposition: false`이고 build module inventory에 runtime source가
|
||||
없어야 한다. `check:optional-recipes`가 이를 강제한다.
|
||||
완전 철회하려면 `src/application/ports/browser-file-storage`,
|
||||
`src/adapters/browser-files`, `src/adapters/browser-file-storage`,
|
||||
`src/adapters/storage/indexeddb`, `src/adapters/storage/opfs`,
|
||||
`src/adapters/cache-storage`와 전용 test를 제거하고 catalog의
|
||||
`referenceRuntime` metadata를 삭제한다.
|
||||
`test:browser-file-storage-removal`은 이 상태에서 base typecheck, architecture,
|
||||
test, build와 optional catalog가 유지되는지 검증한다.
|
||||
|
||||
제품에 composition한 이후 rollback은 다음 순서를 따른다.
|
||||
|
||||
1. 신규 write, worker activation과 cache candidate를 중지한다. 별도 upload
|
||||
workflow를 설치했다면 그 session도 독립적으로 중지한다.
|
||||
2. file ref/object URL/handle/connection/channel을 정리한다.
|
||||
3. offline read-write를 read-only 또는 online-only로 전환한다.
|
||||
4. N-1 bundle이 future schema를 destructive open 없이 감지하는지 확인한다.
|
||||
5. user-authored/unsynced data는 export/sync 확인 없이 purge하지 않는다.
|
||||
6. owned OPFS/cache namespace만 journal/manifest 기준으로 정리한다.
|
||||
7. adapter composition, runtime config와 dependency를 제거한다.
|
||||
|
||||
schema downgrade, blanket `deleteDatabase()`, `caches.keys()` 전체 삭제와 사용자
|
||||
filename 기반 OPFS 삭제는 rollback 수단으로 금지한다.
|
||||
@@ -0,0 +1,202 @@
|
||||
# VD-12: Presigned transfer, resumable upload와 Image CDN 경계
|
||||
|
||||
- 상태: Accepted — reference runtime available, not composed
|
||||
- 결정일: 2026-07-28
|
||||
- catalog recipe availability: `RECIPE_AVAILABLE` (primary status/selection과 별도)
|
||||
- reference runtime 상태: `AVAILABLE_NOT_COMPOSED`
|
||||
- 관련 결정: VD-10, VD-11, VD-14, VD-16
|
||||
- current status ledger:
|
||||
`docs/architecture/browser-data-capability-completion-ledger.md`
|
||||
- 재검토: 제품이 server file upload/download 또는 Image CDN delivery를 선택할 때
|
||||
|
||||
## 배경
|
||||
|
||||
Presigned URL은 URL 문자열이 아니라 짧은 수명의 bearer capability다.
|
||||
multipart/resumable upload는 단순 PUT 반복이 아니라 session, part identity,
|
||||
checksum, authoritative reconciliation, completion과 orphan cleanup protocol이다.
|
||||
streaming download는 전체 payload를 메모리에 올리지 않지만 response binding,
|
||||
truncation/overrun, destination commit과 integrity를 별도로 처리해야 한다.
|
||||
Image CDN URL도 arbitrary transform builder로 노출하면 cache poisoning, pixel/decode
|
||||
bomb, signed-query 유출과 source-fetch SSRF 경계가 사라진다.
|
||||
|
||||
이 네 capability를 범용 `HttpClient`나 `FileService` mega-port 하나로 합치면
|
||||
control plane authorization과 byte data plane, local browser lifecycle과 server
|
||||
authority가 섞인다.
|
||||
|
||||
## 결정
|
||||
|
||||
1. BFF/Web API control plane과 object-storage/CDN data plane을 분리한다.
|
||||
2. 브라우저는 signing key, cloud 관리자 credential, bucket/container, raw object
|
||||
key 생성 규칙을 소유하지 않는다.
|
||||
3. application caller는 raw URL/query/signed headers를 전달하지 않는다.
|
||||
composition/provider가 발급한 exact capability만 adapter가 소비한다.
|
||||
4. presigned capability는 version, opaque identity, method, logical resource 또는
|
||||
session/part, exact URL, origin/path policy, byte/media/checksum 조건과 expiry를
|
||||
immutable하게 binding한다.
|
||||
5. signed URL은 bearer credential로 취급하고 persistence, checkpoint, telemetry,
|
||||
analytics, referrer와 raw exception에 넣지 않는다.
|
||||
6. data-plane fetch는 기본적으로 `credentials: omit`, `redirect: error`,
|
||||
`referrerPolicy: no-referrer`, `cache: no-store`를 사용한다. cross-origin은
|
||||
composition allowlist와 CORS/CSP 계약이 있을 때만 연다.
|
||||
7. client-side single-use 표시는 UX와 accidental replay를 줄이는 보조책이다.
|
||||
cross-tab/replay의 최종 authority는 server 또는 composition-owned atomic
|
||||
consumer다.
|
||||
8. streaming download는 response body를 closed-result stream으로 변환하고
|
||||
output chunk, total bytes, media type, encoding과 선택적 incremental integrity를
|
||||
검증한다. overrun/truncation/abort 시 native reader와 destination을 닫는다.
|
||||
9. `BROWSER_HANDOFF`와 destination close 이후의 `SAVED`를 계속 분리한다.
|
||||
10. Range resumable download는 별도 capability다. `206`, `Content-Range`,
|
||||
validator, destination seek/truncate와 final integrity 없이는 append resume를
|
||||
허용하지 않는다.
|
||||
11. upload 상위 계약은 server-authoritative session/status/part/complete/abort를
|
||||
소유하고 data-plane part executor는 capability 타입에 generic하다. 따라서
|
||||
S3-style presigned multipart와 BFF proxy part를 같은 application contract
|
||||
뒤에 둘 수 있지만 wire DTO를 공유하지 않는다.
|
||||
12. reference upload protocol literal은 `PRESIGNED_MULTIPART_V1`이다. 모든
|
||||
control-plane request/response, session과 checkpoint가 이를 exact하게
|
||||
포함하며 다른 값이나 누락을 거절한다.
|
||||
13. session은 exact source binding, total bytes, media type, part size/count,
|
||||
concurrency, checksum algorithm과 expiry를 묶는다. part number는 1부터
|
||||
연속적이고 offset/length/checksum/idempotency를 정확히 binding한다.
|
||||
14. control transport는 `CREATE_SESSION`, `GET_STATUS`, `COMPLETE`, `ABORT`의
|
||||
closed operation을 composition-owned fixed HTTPS endpoint map으로만
|
||||
실행한다. presigned 발급도 factory에 고정된 단일 BFF endpoint를 사용하며
|
||||
caller-provided URL을 받지 않는다.
|
||||
15. `requestBindingSha256`와 `uploadBindingSha256`는
|
||||
`RESUMABLE-UPLOAD-BINDING-V1` 및
|
||||
`RESUMABLE-UPLOAD-SESSION-BINDING-V1` canonical field sequence의 SHA-256이다.
|
||||
`UPLOAD_PART` capability binding은 exact
|
||||
`protocol: PRESIGNED_MULTIPART_V1`을 포함한다. BFF는 `sessionId`로 server
|
||||
session을 조회하고 snapshot으로 protocol, binding과 part plan을 재계산한다.
|
||||
client digest는 authorization이나 ownership 증명이 아니다.
|
||||
16. part memory는 `partSize × concurrency × copyFactor` hard ceiling으로 제한한다.
|
||||
retry는 같은 bytes/checksum/idempotency에만 허용한다.
|
||||
17. retryable network, 429와 모든 5xx는 bounded attempt/`Retry-After`/abortable
|
||||
backoff 안에서만 재시도한다. status의 404/410 또는
|
||||
`NOT_FOUND`/`EXPIRED`는 terminal로 보고 checkpoint를 CAS 제거한다.
|
||||
18. PUT 성공은 capability-bound status, receipt header,
|
||||
`expectedResponseByteLength`와 exact `Content-Length`를 검증하고 hard cap과
|
||||
deadline 안에서 response body를 끝까지 drain한 뒤에만 확정한다. 204는
|
||||
expected response bytes가 0일 때만 허용하며 `Content-Length` 부재를 0으로
|
||||
정규화한다.
|
||||
19. resume는 local checkpoint만 신뢰하지 않는다. server status를 다시 읽고
|
||||
완료 part의 local range digest와 server checksum/receipt를 대조한 뒤 missing
|
||||
part만 전송한다.
|
||||
20. checkpoint에는 opaque session/source binding과 reconciliation에 필요한
|
||||
protocol-defined SHA-256 file fingerprint, per-part checksum, bounded opaque
|
||||
non-authorizing part receipt token만 저장한다. 이 값도 account partition과
|
||||
retention을 적용하고 diagnostics/telemetry에는 내보내지 않는다. presigned
|
||||
URL, signed header, bearer token/capability, file name, path, account ID,
|
||||
raw provider ETag와 raw server error는 금지한다.
|
||||
21. cancel과 server abort를 분리한다. same-origin 다른 tab의 active upload는
|
||||
strict `RESUMABLE_UPLOAD_CANCEL_V1` BroadcastChannel 신호로 먼저 중단한 뒤
|
||||
per-key Web Lock 안에서 server abort/reconcile을 수행한다. 이 ephemeral
|
||||
신호는 opaque upload key만 운반하고 persistence하지 않으며 authority가
|
||||
아니다. channel이 없으면 abort caller의 bounded signal 아래 lock을 기다린다.
|
||||
complete/abort가 불명확하면 server reconcile 전까지 성공으로 기록하거나
|
||||
checkpoint를 파기하지 않는다.
|
||||
22. multipart complete는 ordered receipt 검증 뒤에도 `QUARANTINED`다. backend
|
||||
scan/CDR/promotion이 끝나기 전 available/public URL을 발급하지 않는다.
|
||||
application-facing 성공값은 state/resource/byte length/replay 여부만 노출하고
|
||||
session ID, request binding과 fingerprint를 제거한다.
|
||||
23. Image CDN application contract는 opaque asset reference와
|
||||
composition-registered named preset만 받는다. arbitrary source URL과 raw
|
||||
transform query는 금지한다.
|
||||
24. asset descriptor는 immutable revision, delivery class, safe raster media,
|
||||
natural dimensions, rendition dimensions/formats/URLs와 private expiry를 묶는다.
|
||||
25. CDN policy는 allowed HTTPS origin/path, preset width/DPR/format/quality/fit,
|
||||
output pixel/decoded-byte/encoded-byte/candidate/lifetime ceiling과
|
||||
cache/referrer policy를 소유한다.
|
||||
composition limit은 exported adapter implementation ceiling을 초과할 수
|
||||
없고 capability verification concurrency도 절대 상한 아래에서 제한한다.
|
||||
CDN origin은 composition이 명시한 application origin과 달라야 한다.
|
||||
`<img crossorigin="anonymous">`가 same-origin 요청에서는 cookie를 보낼 수
|
||||
있기 때문에 private URL의 credential omission을 probe에만 맡기지 않는다.
|
||||
26. private capability signature가 허용하는 값은 versioned preset binding ID다.
|
||||
CDN/BFF는 그 ID를 server-owned immutable preset registry에서 조회하고,
|
||||
요청의 width/height/DPR/fit/format/quality가 그 preset의 exact candidate인지
|
||||
재계산해 하나라도 다르면 거절한다. signed URL에 붙은 raw transform query나
|
||||
client 계산값은 authorization proof가 아니다.
|
||||
signing key policy는 bounded unique `acceptedKeyIds` overlap set이고 verifier
|
||||
registry가 모든 ID를 포함해야 한다. descriptor의 단일 key ID는 양쪽
|
||||
registry에 exact membership이 있어야 한다.
|
||||
27. browser probe는 native decode 전에 PNG/JPEG/WebP/AVIF header metadata와
|
||||
static-only container를 검사한다. 선언 dimensions, pixels와 decoded-byte
|
||||
budget을 넘거나 APNG/WebP animation, AVIF sequence/derived image,
|
||||
ambiguous/malformed container이면 decode 전에 거절한다.
|
||||
28. private signed delivery는 `PRIMARY_REQUIRED` probe를 강제하고
|
||||
`credentials: omit`, exact response URL과 실제 `Cache-Control: no-store`를
|
||||
검증한다. fetch/body/decode 전체에 하나의 timeout을 적용하고 abort/late
|
||||
completion에서 reader와 bitmap을 닫는다.
|
||||
29. SVG/HTML/data/blob/javascript와 unknown active media는 기본 거절한다.
|
||||
animation은 frame/decode budget이 승인된 별도 protocol 전에는 허용하지 않는다.
|
||||
30. responsive candidate는 한 source set에서 하나의 descriptor 종류만 사용하고,
|
||||
고유한 양수 width를 오름차순으로 반환한다. `sizes`는 registry-owned layout
|
||||
token에서 결정한다.
|
||||
31. public rendition은 immutable revision URL과 public immutable cache를 사용하고,
|
||||
private rendition은 short-lived capability와 필수 no-store를
|
||||
사용한다. 같은 URL의 content를 purge로 바꿔치기하지 않는다.
|
||||
32. Image CDN runtime `close()`는 terminal/idempotent다. runtime lifetime
|
||||
signal로 진행 중 verification/probe를 중단하고 accepted WeakMap을 새
|
||||
WeakMap으로 교체해 기존 reference를 즉시 revoke한다. 닫힌 runtime은
|
||||
재개하지 않고 새 composition으로 교체한다.
|
||||
33. 공통 runtime은 concrete browser mechanism과 policy validation을 제공하지만
|
||||
backend endpoint/vendor schema와 제품 asset/upload owner가 없으므로 bootstrap에
|
||||
조합하지 않는다.
|
||||
34. runtime source는 production module inventory와 removal gate로 기본 bundle에서
|
||||
제외됨을 증명한다.
|
||||
35. Range resume의 detailed state machine과 app-managed background의 플랫폼
|
||||
경계는 VD-14가 소유한다. VD-12의 whole-object streaming 구현을 그
|
||||
capability의 구현 증거로 사용하지 않는다.
|
||||
36. top-level transfer runtime, account-scoped teardown, upload pause/inventory,
|
||||
Image descriptor HTTP provider/refresh와 safe presentation projection은
|
||||
VD-16이 소유한다. 개별 runtime factory의 존재를 operational composition
|
||||
완료로 해석하지 않는다.
|
||||
|
||||
## Backend와 맞출 계약
|
||||
|
||||
- fixed BFF capability endpoint, closed session endpoint map과 runtime schema
|
||||
- `PRESIGNED_MULTIPART_V1` canonical binding, server-side session lookup,
|
||||
authorization/revocation
|
||||
- object storage CORS, allowed method/headers, exposed receipt/checksum headers
|
||||
- PUT 성공 status, receipt header, response byte length/body cap
|
||||
- session expiry, 404/410 terminal 의미, list/status pagination, idempotency와
|
||||
orphan cleanup
|
||||
- part/full-object checksum의 정확한 알고리즘·composite 의미
|
||||
- quarantine scan, promotion, status와 reject/delete lifecycle
|
||||
- CDN source registry, immutable asset revision, versioned named preset의 exact
|
||||
candidate 재계산과 query mismatch 거절
|
||||
- image signing key overlap 배포, signer 전환, capability/client drain과
|
||||
emergency revocation/forced rollout runbook
|
||||
- CDN `Content-Type`, static header metadata, dimensions/decoded-byte budget,
|
||||
private `no-store`, application과 분리된 CDN origin, cache key, `Vary`, CORS와 CSP
|
||||
|
||||
브라우저의 local file reference, native `File`/`Blob`, IndexedDB checkpoint physical
|
||||
schema, OPFS path, signed URL query와 cloud object key는 backend 공유 계약이 아니다.
|
||||
|
||||
## 선택하지 않은 대안
|
||||
|
||||
- application caller가 arbitrary presigned URL을 직접 전달
|
||||
- browser bundle에서 cloud signing
|
||||
- 범용 JSON `HttpClient`로 binary streaming/part protocol까지 처리
|
||||
- complete 응답을 scan 완료 또는 public availability로 간주
|
||||
- local checkpoint만 보고 upload complete
|
||||
- ETag를 무조건 MD5/SHA-256으로 해석
|
||||
- private signed image URL을 query cache나 persistence에 장기 저장
|
||||
- raw transform query로 CDN URL 조립
|
||||
- large download의 unbounded Blob fallback
|
||||
|
||||
## 증적
|
||||
|
||||
- application ports: `src/application/ports/browser-transfer/`
|
||||
- concrete adapters: `src/adapters/browser-transfer/`
|
||||
- unit/fault tests: `tests/unit/`
|
||||
- real browser cases: `tests/browser-capabilities/`
|
||||
- 상세 설계:
|
||||
`docs/architecture/presigned-transfer-and-image-cdn.md`
|
||||
- Range/background 결정:
|
||||
`docs/architecture/decisions/VD-14-resumable-download-and-background-transfer.md`
|
||||
- composition/Image provider 결정:
|
||||
`docs/architecture/decisions/VD-16-browser-transfer-composition-and-image-delivery.md`
|
||||
- 운영 복구:
|
||||
`docs/operations/browser-transfer-recovery.md`
|
||||
@@ -0,0 +1,880 @@
|
||||
# VD-13: Client cache scope, persistence와 탭 간 일관성 경계
|
||||
|
||||
- 상태: Accepted — staged implementation required
|
||||
- 결정일: 2026-07-28
|
||||
- 관련 결정: VD-10, VD-11
|
||||
- 상세 설계:
|
||||
`docs/architecture/client-cache-and-storage.md`
|
||||
- current status ledger:
|
||||
`docs/architecture/browser-data-capability-completion-ledger.md`
|
||||
- 재검토:
|
||||
account/tenant switching, query persistence, SSR 또는 offline mutation을
|
||||
제품 capability로 선택할 때
|
||||
|
||||
## 1. 배경
|
||||
|
||||
TanStack Query memory cache, Web Storage, IndexedDB와 BroadcastChannel은 모두
|
||||
client state에 관여하지만 같은 authority, 수명과 commit point를 갖지 않는다.
|
||||
|
||||
- TanStack Query memory cache는 현재 JavaScript runtime의 server-state projection다.
|
||||
- `localStorage`와 `sessionStorage`는 작은 preference/control record를 위한
|
||||
동기식 browser storage다.
|
||||
- IndexedDB는 transaction, index와 durable structured record를 제공한다.
|
||||
- BroadcastChannel과 `storage` event는 같은 storage partition 안의 best-effort
|
||||
notification이다.
|
||||
- SSR dehydration과 browser persistence hydration은 서로 다른 source에서 생성된
|
||||
cache projection을 합치는 별도 protocol이다.
|
||||
|
||||
현재 skeleton은 memory QueryClient, 두 개의 등록 Web Storage key와
|
||||
invalidate-only cross-tab runtime을 production bootstrap에 조립한다. domain-neutral
|
||||
IndexedDB runtime은 source와 native contract test가 있지만 product dataset 없이
|
||||
bootstrap에서 제외돼 있다. IndexedDB query persister, durable namespace epoch,
|
||||
session/account-scoped QueryClient lifecycle과 SSR hydration은 아직 구현되지
|
||||
않았다.
|
||||
|
||||
이 차이를 숨긴 채 “client cache가 구현됐다”고 표현하면 다음 문제가 생긴다.
|
||||
|
||||
- logout 뒤 old account의 cache나 늦은 async result가 새 account 화면에 나타남
|
||||
- best-effort invalidation event를 authorization 또는 server commit으로 오인함
|
||||
- 여러 tab의 full cache snapshot이 서로 오래된 record를 다시 살림
|
||||
- browser persistence가 최신 SSR payload를 덮음
|
||||
- Web Storage memory fallback 성공과 durable write 성공을 구분하지 못함
|
||||
- query cache를 offline command repository처럼 사용해 unsynced user data를
|
||||
eviction으로 잃음
|
||||
|
||||
## 2. 표준 capability 상태
|
||||
|
||||
이 결정과 상세 설계는 다음 상태만 사용한다.
|
||||
|
||||
| 상태 | 의미 |
|
||||
| --- | --- |
|
||||
| `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로 보장하지 못한다. |
|
||||
|
||||
`AVAILABLE_NOT_COMPOSED`와 `NOT_SELECTED`는 같은 말이 아니다. 전자는 reusable
|
||||
runtime의 구현 상태고, 후자는 제품 capability 선택 상태다. 하나의 capability에
|
||||
두 축이 필요하면 “reference runtime”과 “product selection”을 별도 행으로 쓴다.
|
||||
|
||||
### 2.1 현재 상태
|
||||
|
||||
| capability | 현재 상태 | 현재 보장 |
|
||||
| --- | --- | --- |
|
||||
| TanStack Query memory runtime | `COMPOSED` | runtime별 QueryClient, finite inactive GC, retry owner, query AbortSignal |
|
||||
| registered Web Storage | `COMPOSED` | `COLOR_SCHEME`, `CHUNK_RELOAD_GUARD`만 strict codec/envelope로 사용 |
|
||||
| invalidate-only cross-tab runtime | `COMPOSED` | versioned topic, BroadcastChannel 우선, localStorage pulse fallback |
|
||||
| generic IndexedDB repository/maintenance runtime | `AVAILABLE_NOT_COMPOSED` | CAS, idempotency, transaction complete, policy binding, bounded lifecycle/migration |
|
||||
| session/account-scoped QueryClient lifecycle | `DESIGNED_NOT_IMPLEMENTED` | 없음; 현재 cache epoch는 release ID만 포함 |
|
||||
| strict query policy/key codec | `DESIGNED_NOT_IMPLEMENTED` | 현재 object key order canonicalization만 존재 |
|
||||
| IndexedDB query persistence facade | `DESIGNED_NOT_IMPLEMENTED` | persistence는 registry에서 강제로 disabled |
|
||||
| durable namespace invalidation ledger | `DESIGNED_NOT_IMPLEMENTED` | 없음 |
|
||||
| product query persistence | `NOT_SELECTED` | persist 대상 query/owner가 없음 |
|
||||
| SSR dehydration/hydration | `NOT_SELECTED` | 현재 runtime은 client SPA composition |
|
||||
| exactly-once cross-tab delivery | `PLATFORM_LIMITED` | BroadcastChannel/storage event는 acknowledgement를 제공하지 않음 |
|
||||
| browser storage non-eviction guarantee | `PLATFORM_LIMITED` | persist 요청도 user-agent eviction을 절대 금지하지 않음 |
|
||||
|
||||
## 3. 결정
|
||||
|
||||
### 3.1 하나의 cache/storage abstraction으로 합치지 않는다
|
||||
|
||||
다음 경계를 유지한다.
|
||||
|
||||
```text
|
||||
server response
|
||||
-> feature application result
|
||||
-> query inbound adapter
|
||||
-> scope-owned TanStack QueryClient
|
||||
|
||||
small approved preference/control value
|
||||
-> registered Web Storage facade
|
||||
-> exact localStorage/sessionStorage key
|
||||
|
||||
optional reconstructable query projection
|
||||
-> query persistence facade
|
||||
-> query-specific stable wire codec
|
||||
-> governance-bound IndexedDB runtime
|
||||
|
||||
committed mutation
|
||||
-> local namespace invalidation
|
||||
-> optional durable namespace epoch commit
|
||||
-> best-effort cross-tab hint
|
||||
```
|
||||
|
||||
QueryClient, native `Storage`, `IDBDatabase`, BroadcastChannel, dehydrated TanStack
|
||||
types와 physical key/store/index 이름을 application/domain에 노출하지 않는다.
|
||||
|
||||
### 3.2 source of truth와 authority
|
||||
|
||||
1. 일반 server state와 authorization의 source of truth는 서버다.
|
||||
2. memory cache와 persisted query record는 재구성 가능한 projection이다.
|
||||
3. cache hit, persisted restore와 invalidation event는 authorization proof가 아니다.
|
||||
4. 모든 protected network request는 현재 session credential과 server
|
||||
authorization을 다시 통과한다.
|
||||
5. remote invalidation event는 `invalidate`만 요청할 수 있다. `remove`, `clear`,
|
||||
logout, account deletion과 credential revocation authority를 갖지 않는다.
|
||||
6. unsynced command, local-first draft와 user-authored offline data는 query
|
||||
persistence에 저장하지 않는다. feature-specific IndexedDB repository와
|
||||
sync use case가 소유한다.
|
||||
|
||||
## 4. session/account/release scope
|
||||
|
||||
### 4.1 immutable scope snapshot
|
||||
|
||||
composition의 session authority는 다음 의미를 갖는 immutable snapshot을 발급한다.
|
||||
구현 type과 field name은 이 의미를 보존해야 한다.
|
||||
|
||||
```ts
|
||||
type CacheScopeSnapshot = Readonly<{
|
||||
protocolVersion: 1;
|
||||
authorityToken: string;
|
||||
partitionToken: string;
|
||||
sessionEpoch: string;
|
||||
accountEpoch: string;
|
||||
releaseEpoch: string;
|
||||
generation: number;
|
||||
}>;
|
||||
```
|
||||
|
||||
- 모든 token은 registry/session authority가 발급한 충분한 entropy의 opaque
|
||||
identifier다.
|
||||
- email, account/tenant/user ID, domain ID, access token과 낮은 entropy identifier의
|
||||
단순 hash를 사용하지 않는다.
|
||||
- `generation`은 현재 page runtime에서 단조 증가하는 local lifecycle fence다.
|
||||
backend entity revision이나 wire ordering으로 사용하지 않는다.
|
||||
- `sessionEpoch`는 sign-in, re-auth, credential owner 교체 때 바뀐다.
|
||||
- `accountEpoch`는 account/tenant switch, logout, account deletion 때 바뀐다.
|
||||
- `releaseEpoch`는 query-key, mapper, codec 또는 persistence wire compatibility가
|
||||
깨질 때 바뀐다.
|
||||
- scope object와 nested policy는 construction 때 copy/freeze한다. async operation은
|
||||
시작 시 exact snapshot과 generation을 캡처한다.
|
||||
|
||||
### 4.2 profile별 scope projection
|
||||
|
||||
모든 query가 account token을 key에 넣지는 않는다. registry가 분류에 따라 다음을
|
||||
고정한다.
|
||||
|
||||
| scope | binding |
|
||||
| --- | --- |
|
||||
| `ORIGIN_SHARED` | release epoch와 origin-shared token |
|
||||
| `ACCOUNT_BOUND` | partition token, account epoch, release epoch |
|
||||
| `SESSION_BOUND` | partition token, account epoch, session epoch, release epoch |
|
||||
|
||||
- `PUBLIC`만 `ORIGIN_SHARED`를 사용할 수 있다.
|
||||
- `INTERNAL`은 제품 authority가 origin-shared public semantics를 증명하지 않는 한
|
||||
`ACCOUNT_BOUND` 이상이다.
|
||||
- `PERSONAL`은 `ACCOUNT_BOUND` 이상이고 persistence에는 explicit approval,
|
||||
bounded retention과 logout purge가 필요하다.
|
||||
- `CONFIDENTIAL`은 query persistence가 금지되고 필요한 순간의 memory
|
||||
`SESSION_BOUND`만 허용한다.
|
||||
- credential은 memory query data, persistence, query key와 invalidation wire
|
||||
모두에서 금지한다.
|
||||
|
||||
### 4.3 composite cache epoch
|
||||
|
||||
cross-tab `cacheEpoch`는 raw token을 연결한 문자열이 아니라 선택된 scope projection과
|
||||
protocol major의 opaque compatibility fingerprint다. receiver는 exact equality만
|
||||
검사하고 원래 account/session/release 의미 값을 wire에서 복원하지 않는다.
|
||||
|
||||
현재 `release.<releaseId>`만 사용하는 값은 transitional implementation이다.
|
||||
account-dependent query를 production에 설치하기 전에 composite scope fingerprint로
|
||||
교체한다.
|
||||
|
||||
## 5. QueryClient lifecycle와 late-result fence
|
||||
|
||||
### 5.1 runtime state
|
||||
|
||||
scope-owned query runtime은 다음 terminal lifecycle을 갖는다.
|
||||
|
||||
```text
|
||||
CREATING
|
||||
-> ACTIVE
|
||||
-> FENCING
|
||||
-> DISPOSING
|
||||
-> DISPOSED
|
||||
```
|
||||
|
||||
- `ACTIVE`만 신규 query/mutation/cache update를 admission한다.
|
||||
- scope transition이 시작되면 먼저 `FENCING`으로 바꾸고 generation을 올린다.
|
||||
- `DISPOSING`에서 old query를 cancel하고 provider/controller를 detach한 뒤
|
||||
QueryClient를 clear한다.
|
||||
- old cross-tab channel, persistence writer/connection, timer와 listener를 닫는다.
|
||||
- exact old Web Storage key/IndexedDB partition purge는 policy와 authority를
|
||||
통과한 bounded lifecycle operation으로 수행한다.
|
||||
- 새 scope는 새 QueryClient와 새 coordinator를 만든다. old client를 재사용해
|
||||
key prefix만 바꾸지 않는다.
|
||||
- dispose와 scope transition은 idempotent하다.
|
||||
|
||||
### 5.2 query fence
|
||||
|
||||
query execution은 TanStack의 AbortSignal과 scope generation을 모두 캡처한다.
|
||||
|
||||
1. 시작 전 runtime이 `ACTIVE`인지 확인한다.
|
||||
2. application request에 AbortSignal을 전달한다.
|
||||
3. 완료 시 captured generation과 current generation을 비교한다.
|
||||
4. mismatch면 성공/실패 모두 새 cache/UI에 적용하지 않고 `STALE_RESULT`로
|
||||
폐기한다.
|
||||
5. query cancellation 실패가 scope clear를 막지 않게 하되 safe diagnostic을
|
||||
남긴다.
|
||||
|
||||
### 5.3 mutation fence
|
||||
|
||||
frontend abort는 이미 서버에 도달한 mutation을 되돌리지 않는다.
|
||||
|
||||
- 시작 전 admission과 generation을 확인한다.
|
||||
- server commit 전 cancellation은 transport의 idempotency/cancellation 계약을
|
||||
따른다.
|
||||
- server 결과가 old generation에서 돌아오면 새 cache에 optimistic result,
|
||||
invalidation 또는 success UI를 적용하지 않는다.
|
||||
- server side effect의 authoritative 결과는 새 scope에서 정상 revalidation한다.
|
||||
- mutation success 후 local invalidation 실패나 hint publish 실패가 이미 committed
|
||||
server mutation을 실패로 바꾸지 않는다.
|
||||
- conflict resolution은 backend revision/ETag/idempotency 계약과 feature policy가
|
||||
소유한다. query cache는 business merge authority가 아니다.
|
||||
|
||||
### 5.4 session owner 연결
|
||||
|
||||
production bootstrap은 auth/session owner subscription을 query lifecycle에
|
||||
연결한다. 단순 `authenticated` boolean만으로 account identity를 추론하지 않는다.
|
||||
owner는 opaque scope snapshot 또는 이를 발급할 authority를 제공해야 한다.
|
||||
|
||||
다른 tab의 logout은 cache invalidation event에 의존하지 않는다. 각 tab의 auth
|
||||
owner가 credential/session 변화를 독립적으로 감지하고 local lifecycle을
|
||||
실행해야 한다.
|
||||
|
||||
## 6. strict query scope/persistence registry와 key codec
|
||||
|
||||
### 6.1 registry
|
||||
|
||||
모든 installed query namespace는 immutable scope/persistence profile을 갖는다.
|
||||
freshness, GC, refetch, retry, result budget, pagination과 conditional policy의
|
||||
유일한 source of truth는 VD-25 `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;
|
||||
}>[];
|
||||
}>;
|
||||
```
|
||||
|
||||
construction은 최소 다음을 검증한다.
|
||||
|
||||
- namespace/topic/policy ID가 closed syntax와 unique version을 가짐
|
||||
- persistence가 classification, scope, max age와 맞음
|
||||
- `NONE`은 topic 0개, `INVALIDATE`는 namespace당 unique topic 1..8개
|
||||
- `(topicId, topicVersion)` 하나는 최대 32개 namespace에 fan-out하며 global
|
||||
topic→namespace set과 namespace→topic set이 서로 exact inverse
|
||||
- profile과 nested allowlist를 deep snapshot/freeze함
|
||||
- VD-25 query definition/`ServerStateProfile`과 join했을 때 owner,
|
||||
classification, scope, namespace, persistence와 invalidation topic set/version이
|
||||
일치함
|
||||
|
||||
composition은 `QueryDefinition -> QueryScopePersistencePolicy ->
|
||||
ServerStateProfile`을 exact ID로 join한 뒤에만 TanStack option을 만든다. global
|
||||
QueryClient default는 안전 baseline일 뿐이고 installed query의 정책 증거가
|
||||
아니다.
|
||||
|
||||
### 6.2 query key wire subset
|
||||
|
||||
query key factory의 canonical input은 다음만 허용한다.
|
||||
|
||||
- `null`, boolean, finite number, bounded string
|
||||
- 위 값의 dense array
|
||||
- own enumerable data property만 가진 plain/null-prototype object
|
||||
|
||||
다음을 fail-closed로 거절한다.
|
||||
|
||||
- cycle/shared exotic graph
|
||||
- `undefined`, `BigInt`, symbol, function, accessor
|
||||
- `NaN`, infinity, negative zero를 구분하지 않는 암묵 변환
|
||||
- Date, RegExp, Map, Set, class/DOM/native object
|
||||
- File, Blob, ArrayBuffer와 typed array
|
||||
- sparse array
|
||||
- `__proto__`, `prototype`, `constructor` key
|
||||
- 허용 depth/node/part/string/serialized-byte ceiling 초과
|
||||
|
||||
구현 절대 상한:
|
||||
|
||||
| 항목 | 상한 |
|
||||
| --- | ---: |
|
||||
| installed query profile | 256 |
|
||||
| query key top-level part | 16 |
|
||||
| canonical value depth | 8 |
|
||||
| canonical value node | 256 |
|
||||
| 단일 string UTF-8 | 1,024 bytes |
|
||||
| 전체 canonical key UTF-8 | 4,096 bytes |
|
||||
|
||||
제품 profile은 더 낮출 수 있지만 이 상한을 높이려면 ADR amendment와
|
||||
memory/telemetry cardinality evidence가 필요하다.
|
||||
|
||||
normative key layout:
|
||||
|
||||
```text
|
||||
[
|
||||
"query",
|
||||
keySchemaVersion,
|
||||
scopeFingerprint,
|
||||
namespaceName,
|
||||
namespaceVersion,
|
||||
queryDefinitionVersion,
|
||||
canonicalSemanticInput
|
||||
]
|
||||
```
|
||||
|
||||
VD-25는 이 배열을 재정의하지 않고 마지막 두 field의 의미와 pagination
|
||||
projection만 소유한다. query function이 의존하는 모든 non-secret input을
|
||||
포함하되 URL 전체, bearer
|
||||
token, email, filename, human-readable personal label을 넣지 않는다. domain entity
|
||||
identity가 필요하면 backend/product contract가 발급한 opaque ID와 bounded codec을
|
||||
사용한다.
|
||||
|
||||
### 6.3 memory pressure
|
||||
|
||||
`gcTime`은 inactive retention이지 active cache hard cap이 아니다.
|
||||
|
||||
- gateway/mapper가 response count/byte ceiling을 검증한다.
|
||||
- binary, native object와 unbounded collection을 query cache에 넣지 않는다.
|
||||
- cache entry/active/inactive와 estimated payload를 safe bucket으로 관측한다.
|
||||
- hard eviction controller는 joined VD-25 profile별 정책으로만 설치한다.
|
||||
- memory pressure를 이유로 active personal data를 arbitrary global timer로
|
||||
삭제하지 않는다. scope lifecycle의 remove/clear와 일반 eviction을 구분한다.
|
||||
|
||||
## 7. Web Storage contract
|
||||
|
||||
### 7.1 registered key policy
|
||||
|
||||
Web Storage는 registered small value 전용이다.
|
||||
|
||||
```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:
|
||||
| Readonly<{ kind: "DISCARD" }>
|
||||
| Readonly<{ kind: "ADJACENT"; migrationId: string }>;
|
||||
quotaFallback: "MEMORY" | "NO_PERSIST" | "FEATURE_DISABLE";
|
||||
logoutAction: "KEEP" | "PURGE_PARTITION";
|
||||
}>;
|
||||
```
|
||||
|
||||
구현 절대 상한:
|
||||
|
||||
| 항목 | 상한 |
|
||||
| --- | ---: |
|
||||
| registered persistent key | 64 |
|
||||
| key별 serialized value | 16,384 bytes |
|
||||
| 한 sweep에서 검사할 key | 16 |
|
||||
| 한 read에서 migration step | 2 |
|
||||
|
||||
현재 두 key는 각각 더 좁은 codec을 유지한다. `COLOR_SCHEME`은 public
|
||||
origin-shared preference이고 `CHUNK_RELOAD_GUARD`는 tab session control이다.
|
||||
server response, credential, signed URL, File/Blob, large draft와 queue를 Web
|
||||
Storage에 넣지 않는다.
|
||||
|
||||
### 7.2 physical identity와 envelope
|
||||
|
||||
physical key는 application/environment, scope kind, opaque partition 또는 tab
|
||||
instance, logical key, schema version에서 결정적으로 파생한다. account/user ID를
|
||||
포함하거나 origin 전체 key를 열거하지 않는다.
|
||||
|
||||
partition-aware 새 envelope는 기존 v1 세 필드의 의미를 변경하지 않고 새
|
||||
envelope version으로 도입한다.
|
||||
|
||||
```ts
|
||||
type BrowserStorageEnvelopeV2 = Readonly<{
|
||||
envelopeVersion: 2;
|
||||
schemaVersion: number;
|
||||
scopeFingerprint: string;
|
||||
writtenAtEpochMs: number;
|
||||
expiresAtEpochMs: number | null;
|
||||
value: unknown;
|
||||
}>;
|
||||
```
|
||||
|
||||
- exact field set, schema, scope, codec, written/expiry time 순으로 검증한다.
|
||||
- TTL expiry는 write time과 registry max age에서 계산하며 caller가 직접 주지 않는다.
|
||||
- 비정상적으로 먼 expiry, future write time과 clock skew는 fail-closed miss다.
|
||||
- corrupt/expired/future/wrong-scope record는 exact key만 best-effort 제거한다.
|
||||
- cleanup 실패는 validated miss를 raw exception으로 바꾸지 않는다.
|
||||
- memory overlay도 exact envelope와 TTL/scope validation을 공유한다.
|
||||
|
||||
### 7.3 read/write outcome
|
||||
|
||||
stored `undefined`와 miss를 암묵적으로 합치지 않는다.
|
||||
|
||||
```ts
|
||||
type WebStorageReadResult<Value> =
|
||||
| Readonly<{ ok: true; state: "HIT"; value: Value; durability: "PERSISTED" | "MEMORY_ONLY" }>
|
||||
| Readonly<{ ok: true; state: "MISS" }>
|
||||
| Readonly<{ ok: false; error: ClientStorageFailure }>;
|
||||
|
||||
type WebStorageWriteResult =
|
||||
| Readonly<{ ok: true; durability: "PERSISTED" }>
|
||||
| Readonly<{ ok: true; durability: "MEMORY_ONLY"; degraded: true }>
|
||||
| Readonly<{ ok: false; error: ClientStorageFailure }>;
|
||||
```
|
||||
|
||||
memory fallback이 current runtime에서 승인된 성공이면 `ok: true`와
|
||||
`MEMORY_ONLY`를 반환한다. durable write가 필수인 key는 fallback을 성공으로
|
||||
가장하지 않는다.
|
||||
|
||||
### 7.4 migration, quota와 sweep
|
||||
|
||||
- migration은 registry에 등록된 deterministic adjacent version만 실행한다.
|
||||
- migration callback은 network/native storage/telemetry side effect 없이 bounded
|
||||
pure codec으로 동작한다.
|
||||
- future version과 unsupported old version은 `DISCARD` policy에서 miss다.
|
||||
- `QuotaExceededError`이면 reconstructable exact key cleanup 뒤 동일 idempotent
|
||||
write를 최대 한 번 재시도한다.
|
||||
- origin 전체 `clear()`와 arbitrary LRU key enumeration을 금지한다.
|
||||
- TTL은 visibility rule이므로 boot/idle/focus 중 registry-owned bounded sweep을
|
||||
별도로 수행한다.
|
||||
- logout/account switch는 exact partition key만 purge한다. public origin-shared
|
||||
preference를 지우지 않는다.
|
||||
- `sessionStorage` opener snapshot을 authority로 사용하지 않는다. tab-local
|
||||
control에는 새 tab instance와 `noopener` policy를 적용한다.
|
||||
|
||||
## 8. optional IndexedDB query persistence
|
||||
|
||||
### 8.1 selection
|
||||
|
||||
query persistence reference facade의 현재 상태는
|
||||
`DESIGNED_NOT_IMPLEMENTED`, product selection은 `NOT_SELECTED`다. 단순 warm-start
|
||||
기대만으로 자동 설치하지 않는다.
|
||||
|
||||
다음 조건을 모두 충족한 query만 등록한다.
|
||||
|
||||
- server-authoritative이며 재구성 가능함
|
||||
- stable query-key와 payload codec이 있음
|
||||
- classification/scope/retention owner 승인
|
||||
- entry/dataset/restore byte와 count budget이 있음
|
||||
- logout/account deletion/release busting이 정의됨
|
||||
- measured offline/warm-start 가치가 있음
|
||||
- three-engine native contract와 rollback evidence가 있음
|
||||
|
||||
### 8.2 stable record, raw TanStack snapshot 금지
|
||||
|
||||
full QueryClient snapshot이나 library-private object를 그대로 저장하지 않는다.
|
||||
|
||||
```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;
|
||||
}>;
|
||||
```
|
||||
|
||||
- approved successful query data만 저장한다.
|
||||
- error, pending state, mutation, function, Promise, AbortSignal, native/binary
|
||||
object, credential와 capability를 저장하지 않는다.
|
||||
- query key와 payload를 각각 strict codec으로 검증한다.
|
||||
- generic IndexedDB runtime의 opaque scope/policy binding, transaction complete,
|
||||
CAS, byte budget, migration, lifecycle와 failure mapping을 재사용한다.
|
||||
|
||||
### 8.3 구현 상한
|
||||
|
||||
reference facade의 기본 절대 상한:
|
||||
|
||||
| 항목 | 상한 |
|
||||
| --- | ---: |
|
||||
| persisted query record | 1,024 |
|
||||
| 단일 encoded entry | 512 KiB |
|
||||
| query persistence dataset | 32 MiB |
|
||||
| 한 restore record | 256 |
|
||||
| 한 restore decoded bytes | 8 MiB |
|
||||
| boot restore deadline | 2,000 ms |
|
||||
| max age | 7 days |
|
||||
| write debounce | 250–2,000 ms |
|
||||
|
||||
제품 policy는 더 낮출 수 있다. 상한 확대는 memory/quota/startup-latency evidence와
|
||||
ADR amendment가 필요하다.
|
||||
|
||||
### 8.4 durable namespace epoch
|
||||
|
||||
full snapshot last-write-wins를 금지한다. 기본 writer model은 shared per-query
|
||||
record + monotonic namespace epoch다.
|
||||
|
||||
```ts
|
||||
type DurableCacheLedger = Readonly<{
|
||||
ledgerVersion: 1;
|
||||
scopeFingerprint: string;
|
||||
releaseEpoch: string;
|
||||
namespaces: Readonly<Record<string, number>>;
|
||||
revision: number;
|
||||
}>;
|
||||
```
|
||||
|
||||
- mutation invalidation은 namespace epoch를 같은 IndexedDB transaction에서
|
||||
증가시킨 뒤 cross-tab hint를 publish한다.
|
||||
- persisted record의 namespace epoch가 ledger보다 작으면 hydrate하지 않는다.
|
||||
- record write는 current ledger epoch와 revision을 CAS 검증한다.
|
||||
- BroadcastChannel sequence나 wall clock을 global durable ordering으로 사용하지
|
||||
않는다.
|
||||
- localStorage read-modify-write counter와 best-effort leader election을 correctness
|
||||
fence로 쓰지 않는다.
|
||||
- ledger commit 뒤 hint를 publish한다. hint가 먼저 나가면 receiver가 commit 전
|
||||
record를 읽을 수 있다.
|
||||
|
||||
### 8.5 restore와 hydration order
|
||||
|
||||
1. bounded deadline으로 IndexedDB를 연다.
|
||||
2. immutable dataset/scope/release binding을 검증한다.
|
||||
3. ledger와 record schema/codec/TTL/byte cap을 검증한다.
|
||||
4. approved profile과 current namespace epoch만 decode한다.
|
||||
5. current memory/SSR state와 precedence를 적용한다.
|
||||
6. hydrate 뒤 normal stale/refetch policy를 실행한다.
|
||||
|
||||
wrong scope, expired, busted와 corrupt reconstructable record는 cache miss로
|
||||
격하하고 exact bounded cleanup한다. persistence unavailable/blocked/timeout은
|
||||
제품이 optional로 선택했다면 memory+network `ONLINE_ONLY`로 fail open한다.
|
||||
offline-required workflow를 query persistence로 가장하지 않는다.
|
||||
|
||||
### 8.6 writer lifecycle
|
||||
|
||||
- cache events는 bounded debounce/coalescing한다.
|
||||
- writer 하나에서 concurrent save를 serialize하고 superseded write를 버린다.
|
||||
- `pagehide`/`beforeunload` transaction 완료를 보장으로 간주하지 않는다.
|
||||
- 정상 runtime 중 주기적으로 commit하고 unload flush는 보조 수단이다.
|
||||
- dispose는 timer를 취소하고 connection/listener를 닫는다.
|
||||
- 아직 transaction complete가 아닌 write를 persisted success로 기록하지 않는다.
|
||||
|
||||
## 9. cross-tab invalidation
|
||||
|
||||
### 9.1 authority
|
||||
|
||||
cross-tab wire는 payload/query-key-free invalidate hint만 전달한다.
|
||||
|
||||
- query state/data replication 금지
|
||||
- authorization/logout/server commit 증명 금지
|
||||
- distributed lock/leader election 금지
|
||||
- exactly-once/ordered delivery 주장 금지
|
||||
- offline command 전송 금지
|
||||
|
||||
remote hint는 registry topic을 local namespace로 해석해 active query를
|
||||
invalidate/refetch한다. inactive query는 다음 mount/focus/freshness 정책에서
|
||||
revalidate한다. remote hint는 `removeQueries`, `clear()` 또는 session transition을
|
||||
직접 실행하지 않는다.
|
||||
|
||||
### 9.2 transport와 source validation
|
||||
|
||||
```text
|
||||
BroadcastChannel
|
||||
-> construction/post failure
|
||||
-> registered localStorage pulse + storage event
|
||||
-> failure/unavailable
|
||||
-> DEGRADED_LOCAL_ONLY + normal stale/focus/reconnect
|
||||
```
|
||||
|
||||
- current 2,048-byte exact wire envelope와 bounded TTL/dedupe/source tracking을
|
||||
유지한다.
|
||||
- topic registry 수에도 query profile과 같은 256개 절대 상한을 적용한다.
|
||||
- localStorage fallback key를 Web Storage control registry에 등록한다.
|
||||
- receiver는 exact key, exact `storageArea === localStorage`, exact composite
|
||||
cache epoch와 event codec을 검증한다.
|
||||
- `sessionStorage`를 cross-tab fallback으로 사용하지 않는다.
|
||||
- publisher는 local invalidation을 직접 수행한다.
|
||||
- publish success는 receiver acknowledgement가 아니다.
|
||||
- BroadcastChannel과 storage 양쪽 delivery는 event ID로 dedupe한다.
|
||||
- per-source sequence gap은 global order 증명이 아니라 “hint를 잃었을 수 있음”을
|
||||
나타낸다.
|
||||
|
||||
### 9.3 lost hint
|
||||
|
||||
query persistence가 꺼져 있으면 finite stale time, focus/reconnect와 manual refresh가
|
||||
eventual revalidation을 제공한다. persistence가 켜져 있으면 visibility/focus와
|
||||
sequence gap에서 durable namespace ledger를 bounded refresh한다.
|
||||
|
||||
즉시 global consistency가 업무 invariant라면 browser bus만으로 충족하지 않는다.
|
||||
backend revision/ETag, server push stream 또는 feature sync protocol을 추가한다.
|
||||
|
||||
## 10. SSR 선택 경계
|
||||
|
||||
현재 SSR product capability는 `NOT_SELECTED`다. browser-only code가 있다는 이유로
|
||||
SSR support가 구현됐다고 주장하지 않는다.
|
||||
|
||||
SSR을 선택하면 별도 implementation gate에서 다음을 모두 구현한다.
|
||||
|
||||
1. HTTP request마다 새 QueryClient를 생성하고 response 뒤 폐기한다.
|
||||
2. server process에서 Web Storage, IndexedDB와 BroadcastChannel에 접근하지 않는다.
|
||||
3. approved successful query만 dehydrate한다.
|
||||
4. serialized state를 HTML context에 안전하게 escape하고 byte/count cap을 적용한다.
|
||||
5. browser의 최신 SSR payload가 old persisted projection보다 우선한다.
|
||||
6. persisted state merge는 missing approved query만 복원하거나 explicit server
|
||||
revision을 비교한다.
|
||||
7. browser storage read 때문에 initial server/client markup이 달라지지 않게
|
||||
hydration-safe bootstrap 단계에서 restore한다.
|
||||
8. request A의 QueryClient/data/scope가 request B에 공유되지 않는 test를 둔다.
|
||||
|
||||
SSR support와 IndexedDB query persistence는 서로 독립 선택이다.
|
||||
|
||||
## 11. privacy와 encryption
|
||||
|
||||
- credential, token, signed URL, authorization header, password와 crypto key는
|
||||
memory query key/data, Web Storage, query persistence와 invalidation wire에
|
||||
넣지 않는다.
|
||||
- logical/physical key, query key/hash input, payload, account/user ID, URL과 native
|
||||
exception message/stack을 telemetry에 보내지 않는다.
|
||||
- 같은 origin JavaScript가 ciphertext와 key를 모두 읽을 수 있는 client-side
|
||||
encryption은 XSS authorization boundary가 아니다.
|
||||
- external/non-extractable key lifecycle과 compliance requirement가 있는 제품은
|
||||
encryption을 defense-in-depth로 별도 선택할 수 있지만, 금지 classification을
|
||||
허용하는 근거가 되지 않는다.
|
||||
- logout purge는 confidentiality의 유일한 방어가 아니다. wrong-scope binding은
|
||||
crash로 old bytes가 남아도 새 runtime이 읽지 못하게 해야 한다.
|
||||
|
||||
## 12. failure와 observability
|
||||
|
||||
failure는 최소 operation, closed code, retry owner, effect certainty와 fallback을
|
||||
표현한다.
|
||||
|
||||
- `ABORTED`와 `DEADLINE_EXCEEDED`를 구분한다.
|
||||
- IndexedDB transaction `complete`만 `APPLIED`다.
|
||||
- Broadcast publish success의 remote effect는 `UNKNOWN`이다.
|
||||
- memory fallback과 persisted success를 구분한다.
|
||||
- optional persistence failure는 `ONLINE_ONLY`로 degrade할 수 있다.
|
||||
- scope mismatch/corruption/future version은 raw record를 반환하지 않는다.
|
||||
- diagnostics failure가 query, storage, lifecycle와 cleanup을 실패시키지 않는다.
|
||||
|
||||
safe metric:
|
||||
|
||||
- memory active/inactive/estimated-byte bucket
|
||||
- Web Storage hit/miss/degraded/quota bucket
|
||||
- persistence restore success/miss/busted/corrupt/deadline bucket
|
||||
- scope reset duration/cleanup-incomplete
|
||||
- invalidation publish/receive/drop/duplicate/gap/coalesced bucket
|
||||
- listener/channel/connection leak count
|
||||
|
||||
## 13. implementation gate
|
||||
|
||||
### Gate 0 — 상태와 문서
|
||||
|
||||
- 이 ADR과 상세 설계가 current/target 상태를 분리한다.
|
||||
- capability catalog, runbook과 test evidence의 상태가 같은 taxonomy를 사용한다.
|
||||
- 구현되지 않은 target type을 current API처럼 문서화하지 않는다.
|
||||
|
||||
### Gate 1 — strict registry와 codec
|
||||
|
||||
- query policy registry와 query key closed codec 구현
|
||||
- profile/key absolute ceiling 구현
|
||||
- Web Storage per-key cap, HIT/MISS/durability result 구현
|
||||
- current v1 key의 discard/upgrade 전략 확정
|
||||
- hostile/cyclic/oversize/property-accessor test 통과
|
||||
|
||||
이 gate는 scope lifecycle을 자동 활성화하지 않는다.
|
||||
|
||||
### Gate 2 — scope-owned QueryClient lifecycle
|
||||
|
||||
- session authority scope snapshot contract 구현
|
||||
- auth owner subscription과 local generation fence 구현
|
||||
- old query cancel/provider detach/client clear/dispose 구현
|
||||
- late query/mutation result 폐기 구현
|
||||
- account switch/logout exact partition cleanup 구현
|
||||
- two-account and lost-event tests 통과
|
||||
|
||||
account-dependent query promotion은 이 gate 전 금지한다.
|
||||
|
||||
### Gate 3 — cross-tab scope hardening
|
||||
|
||||
- composite cache epoch 구현
|
||||
- registered localStorage pulse와 storageArea 검증 구현
|
||||
- browser production coordinator E2E와 bfcache/StrictMode leak test
|
||||
- Chromium/Firefox/WebKit 동일 case evidence
|
||||
|
||||
### Gate 4 — optional query persistence reference runtime
|
||||
|
||||
- stable query record codec와 IndexedDB facade 구현
|
||||
- durable namespace ledger/CAS/commit-before-hint 구현
|
||||
- bounded restore/write/dispose 구현
|
||||
- wrong-scope/TTL/release/migration/quota/blocked test
|
||||
- production bootstrap import와 DB open이 없는 module-inventory/removal gate
|
||||
|
||||
완료 뒤에도 product selection은 `NOT_SELECTED`이고 reference 상태만
|
||||
`AVAILABLE_NOT_COMPOSED`로 바뀐다.
|
||||
|
||||
### Gate 5 — product composition
|
||||
|
||||
- measured requirement와 owner 승인
|
||||
- exact query profile/persistence allowlist/retention/budget 등록
|
||||
- account/logout/backend conflict contract 승인
|
||||
- disabled → canary → enabled traffic admission
|
||||
- rollback, cleanup-only release와 operational drill
|
||||
|
||||
### Gate 6 — SSR 또는 offline workflow
|
||||
|
||||
각 capability를 별도 선택하고 별도 gate를 통과한다.
|
||||
|
||||
- SSR: request isolation, safe dehydration, precedence와 hydration test
|
||||
- offline mutation: feature repository, server idempotency/revision/sync protocol,
|
||||
conflict/export/recovery UX
|
||||
|
||||
query persistence gate 통과가 SSR/offline workflow 통과를 의미하지 않는다.
|
||||
|
||||
## 14. test와 promotion evidence
|
||||
|
||||
### 14.1 deterministic
|
||||
|
||||
- independent QueryClient per runtime/scope
|
||||
- session/account/release transition과 late result
|
||||
- query key hostile value/ceiling/canonical equality
|
||||
- Web Storage HIT/MISS/durability, TTL, migration, quota, cleanup, partition
|
||||
- IndexedDB transaction complete, CAS, ledger epoch와 concurrent writer
|
||||
- hint commit ordering, duplicate/self/stale/gap/coalescing
|
||||
- diagnostics redaction와 dispose leak 0
|
||||
|
||||
### 14.2 real browser
|
||||
|
||||
Chromium, Firefox와 WebKit에서 같은 case set을 실행한다.
|
||||
|
||||
- native BroadcastChannel two-page delivery
|
||||
- localStorage fallback과 exact storageArea
|
||||
- account switch 중 in-flight query
|
||||
- event loss 뒤 local auth lifecycle
|
||||
- IndexedDB concurrent writer/blocked/versionchange/restore deadline
|
||||
- bfcache/pagehide/StrictMode listener·connection cleanup
|
||||
- sessionStorage tab/opener semantics
|
||||
- N-1 release reader와 incompatible buster
|
||||
|
||||
현재 native transport spec이 존재해도 production QueryClient lifecycle 전체와
|
||||
세 engine promotion artifact가 없으면 Gate 3 완료로 보지 않는다.
|
||||
|
||||
### 14.3 promotion artifact
|
||||
|
||||
artifact는 engine/browser version/OS image/build/release ID/contract suite
|
||||
version/pass/fail/skip/실행 시각을 보존한다. fake/jsdom 통과를 native provider
|
||||
통과로 보고하지 않는다. WebKit system dependency 부족은 capability skip이 아니라
|
||||
promotion evidence 미충족이다.
|
||||
|
||||
## 15. rollout과 rollback
|
||||
|
||||
### 15.1 rollout
|
||||
|
||||
1. strict registry/codec을 기존 behavior 뒤 shadow validation으로 배포한다.
|
||||
2. scope lifecycle을 single-account environment에서 먼저 관측한다.
|
||||
3. account switch/logout fault test 뒤 account-dependent query를 허용한다.
|
||||
4. optional persistence는 source와 test만 추가하고 production composition은
|
||||
계속 끈다.
|
||||
5. product selection 뒤 read-only restore/shadow write를 먼저 검증한다.
|
||||
6. 작은 cohort에서 write/restore/quota/blocked/rollback drill을 수행한다.
|
||||
7. error budget과 N-1 compatibility를 확인한 뒤 확대한다.
|
||||
|
||||
### 15.2 kill switch
|
||||
|
||||
서로 독립적으로 끌 수 있어야 한다.
|
||||
|
||||
- persistence restore off
|
||||
- persistence write off
|
||||
- cross-tab publish off
|
||||
- cross-tab receive off
|
||||
- offline mutation admission off
|
||||
|
||||
memory QueryClient와 정상 server fetch는 유지한다. account/session local lifecycle은
|
||||
security boundary이므로 best-effort invalidation kill switch와 함께 끄지 않는다.
|
||||
|
||||
### 15.3 rollback
|
||||
|
||||
1. 신규 persistence write/restore admission을 중지한다.
|
||||
2. writer/timer/channel/listener/DB connection을 dispose한다.
|
||||
3. scope fence와 memory QueryClient clear는 유지한다.
|
||||
4. rollback bundle이 future record/schema를 miss/online-only로 처리하게 한다.
|
||||
5. cleanup-only compatible release에서 exact owned partition을 bounded purge한다.
|
||||
6. retention/rollback window 뒤 registry/adapter/dependency를 제거한다.
|
||||
|
||||
schema version을 내리거나 origin 전체 `localStorage.clear()`/
|
||||
`indexedDB.deleteDatabase()`를 자동 실행하지 않는다. unsynced user-authored data는
|
||||
export/sync 확인 없이 query-cache cleanup으로 삭제하지 않는다.
|
||||
|
||||
## 16. 완료 기준
|
||||
|
||||
- [ ] session/account/release scope가 QueryClient, key와 event에 binding된다.
|
||||
- [ ] scope transition은 admission fence, cancel, detach, clear, dispose와 새
|
||||
QueryClient 생성으로 완료된다.
|
||||
- [ ] old generation query/mutation result가 새 scope UI/cache를 변경하지 않는다.
|
||||
- [ ] strict query scope/persistence registry와 closed key codec이 모든 absolute
|
||||
ceiling을 강제하고 VD-25 profile과 exact join된다.
|
||||
- [ ] Web Storage가 per-key cap, HIT/MISS, durability, partition, logout,
|
||||
migration과 bounded sweep을 구현한다.
|
||||
- [ ] localStorage invalidation fallback이 registered key와 exact storageArea를
|
||||
검증한다.
|
||||
- [ ] Chromium/Firefox/WebKit production coordinator/account lifecycle evidence가
|
||||
있다.
|
||||
- [ ] optional query persistence reference facade는 stable record와 durable
|
||||
namespace ledger를 사용하고 production 미선택 시 zero side effect다.
|
||||
- [ ] persisted mutation/error/native object/credential이 없음을 negative test가
|
||||
증명한다.
|
||||
- [ ] SSR을 선택한 경우 request isolation과 hydration precedence가 증명된다.
|
||||
- [ ] offline mutation을 선택한 경우 backend idempotency/revision/sync와
|
||||
conflict/recovery UX가 별도 계약으로 증명된다.
|
||||
- [ ] rollout/kill-switch/rollback/removal artifact가 보존된다.
|
||||
|
||||
현재 이 체크리스트는 완료 선언이 아니라 implementation gate다. 각 행을 실제
|
||||
source, deterministic test, native evidence와 composition inventory로 증명하기
|
||||
전에는 완료로 바꾸지 않는다.
|
||||
|
||||
## 17. 선택하지 않은 대안
|
||||
|
||||
- module singleton QueryClient
|
||||
- account switch에서 query key prefix만 교체
|
||||
- BroadcastChannel logout event를 lifecycle authority로 사용
|
||||
- arbitrary query key와 raw TanStack cache snapshot persistence
|
||||
- localStorage full-cache snapshot 또는 monotonic counter
|
||||
- browser persistence를 offline command queue로 사용
|
||||
- same-origin client encryption을 credential authorization boundary로 사용
|
||||
- origin 전체 storage clear를 quota/logout/rollback 복구로 사용
|
||||
- fake browser test만으로 production promotion
|
||||
|
||||
## 18. 결과
|
||||
|
||||
장점:
|
||||
|
||||
- account/session boundary와 best-effort invalidation의 권한 차이가 명확하다.
|
||||
- persistence를 선택하지 않은 제품에는 DB open/listener/bundle side effect가 없다.
|
||||
- query key, Web Storage와 IndexedDB의 migration/retention을 독립적으로 검증한다.
|
||||
- old tab/snapshot이 invalidated data를 되살리는 경로를 durable epoch로 닫는다.
|
||||
- SSR, offline workflow와 query warm-start를 서로 독립 선택할 수 있다.
|
||||
|
||||
비용:
|
||||
|
||||
- scope authority와 QueryClient remount lifecycle이 필요하다.
|
||||
- registry/codec/historical fixture와 multi-page browser test가 늘어난다.
|
||||
- optional persistence를 설치하는 제품은 IndexedDB migration, quota와 cleanup
|
||||
runbook을 운영해야 한다.
|
||||
|
||||
이 비용은 cache hit, durable restore, cross-tab hint와 server truth를 하나의
|
||||
“cached” 상태로 잘못 합치지 않기 위한 의도적인 비용이다.
|
||||
@@ -0,0 +1,898 @@
|
||||
# VD-14: Resumable download와 background download 경계
|
||||
|
||||
- 상태: Accepted — production design complete, implementation pending
|
||||
- 결정일: 2026-07-28
|
||||
- catalog recipe availability: `RECIPE_AVAILABLE` (primary status/selection과 별도)
|
||||
- Range resumable download primary status: `DESIGNED_NOT_IMPLEMENTED`
|
||||
- app-managed background download primary status: `NOT_SELECTED`
|
||||
- cross-browser app-managed background download guarantee: `PLATFORM_LIMITED`
|
||||
- 관련 결정: VD-10, VD-11, VD-12
|
||||
- current status ledger:
|
||||
`docs/architecture/browser-data-capability-completion-ledger.md`
|
||||
- 재검토: 제품이 Range 재개, 탭 종료 뒤 전달 또는 대용량 Safari fallback을
|
||||
선택할 때
|
||||
|
||||
## 1. 배경과 현재 사실
|
||||
|
||||
현재 reference runtime은 whole-object `200` response를 bounded stream으로 읽어
|
||||
foreground destination에 저장하거나 browser download manager에 handoff한다. 이
|
||||
경로는 전체 payload를 하나의 `Blob`으로 만들지 않고 byte length와 SHA-256을
|
||||
검증하지만, 네트워크나 탭이 중단되면 다음 실행은 byte 0부터 다시 시작한다.
|
||||
|
||||
Range resume는 기존 stream에 `Range` header 하나를 추가하는 기능이 아니다.
|
||||
representation identity, exact `206 Content-Range`, durable partial destination,
|
||||
checkpoint CAS, `200/412/416` reconciliation과 마지막 whole-object integrity가
|
||||
하나의 protocol이어야 한다. background download도 Range resume와 동일하지 않다.
|
||||
브라우저 download manager에 넘기는 것과 애플리케이션이 Service Worker에서
|
||||
전송을 계속 관리하는 것은 완료 증거와 상호운용성이 전혀 다르다.
|
||||
|
||||
이 ADR은 목표 계약을 정의한다. 이 문서가 존재한다는 사실은 runtime, endpoint,
|
||||
worker 또는 제품 UX가 구현·조합되었다는 뜻이 아니다.
|
||||
|
||||
## 2. 표준 capability 상태
|
||||
|
||||
설계, source 존재, 제품 조합과 플랫폼 한계를 하나의 `enabled` boolean으로 합치지
|
||||
않는다. primary current status는 다음 다섯 값 중 정확히 하나다. 이 taxonomy는
|
||||
선형 maturity model이 아니며 상태 이름만으로 rollout 또는 production readiness를
|
||||
추론하지 않는다.
|
||||
|
||||
| primary status | 의미 |
|
||||
| --- | --- |
|
||||
| `NOT_SELECTED` | 제품 요구, owner, policy 또는 구현 범위가 아직 선택되지 않음 |
|
||||
| `DESIGNED_NOT_IMPLEMENTED` | versioned contract와 불변조건은 승인됐지만 reference source가 없음 |
|
||||
| `AVAILABLE_NOT_COMPOSED` | 검증 가능한 reference source가 있지만 제품 bootstrap/endpoint에는 연결되지 않음 |
|
||||
| `COMPOSED` | 특정 제품 facade, config와 dependency에 실제로 조합됨 |
|
||||
| `PLATFORM_LIMITED` | 요구 semantics를 target browser/platform 전체에서 보장할 수 없음 |
|
||||
|
||||
production readiness와 traffic admission은 primary status와 독립된 축이다.
|
||||
운영 상태는 completion ledger가 정의한 네 canonical 축만 사용한다.
|
||||
|
||||
```text
|
||||
Selection =
|
||||
NOT_SELECTED | SELECTED | REMOVING
|
||||
TrafficAdmission =
|
||||
DISABLED | SHADOW | CANARY | ENABLED
|
||||
RuntimeHealth =
|
||||
UNKNOWN | AVAILABLE | DEGRADED | UNAVAILABLE | INCOMPATIBLE
|
||||
PromotionEvidence =
|
||||
MISSING | PARTIAL | COMPLETE | EXPIRED
|
||||
```
|
||||
|
||||
아래 `source evidence`는 ADR과 reference source의 존재를 설명하는 문서 표기일
|
||||
뿐 canonical readiness 축이 아니다. `DESIGN_REVIEWED`는 native browser나
|
||||
provider 증거가 아니고, `REFERENCE_TESTED`도 ledger의 browser component를
|
||||
`PROMOTABLE` 또는 `PromotionEvidence=COMPLETE`로 만들지 않는다.
|
||||
|
||||
현재 capability별 판정:
|
||||
|
||||
| capability | primary status | source evidence | 비고 |
|
||||
| --- | --- | --- | --- |
|
||||
| whole-object foreground streaming | `AVAILABLE_NOT_COMPOSED` | `REFERENCE_TESTED` | 기존 VD-12 범위 |
|
||||
| browser-managed handoff mechanism | `AVAILABLE_NOT_COMPOSED` | `REFERENCE_TESTED` | 실제 capability issuer는 제품 연결 시 필요 |
|
||||
| Range resumable download | `DESIGNED_NOT_IMPLEMENTED` | `DESIGN_REVIEWED` | 이 ADR의 구현 대상 |
|
||||
| app-managed background download | `NOT_SELECTED` | `DESIGN_REVIEWED` | 제품 요구가 선택될 때만 별도 구현 |
|
||||
| cross-browser app-managed background download guarantee | `PLATFORM_LIMITED` | `DESIGN_REVIEWED` | 공통 baseline으로 promotion 불가 |
|
||||
|
||||
기존 foreground stream과 browser-managed handoff의 source/evidence를 Range나
|
||||
app-managed background download 구현 증거로 재사용하지 않는다.
|
||||
|
||||
## 3. 결정 요약
|
||||
|
||||
1. whole-object foreground streaming, Range resumable download,
|
||||
browser-managed handoff와 app-managed background download를 서로 다른
|
||||
capability와 결과 타입으로 유지한다.
|
||||
2. Range protocol literal은 `RANGE_RESUMABLE_DOWNLOAD_V1`로 고정한다. 기존
|
||||
whole-object presigned contract에 암묵적으로 섞지 않는다.
|
||||
3. resume의 authority는 server-owned immutable generation과 strong validator다.
|
||||
local offset, file name, timestamp 또는 partial byte 존재는 authority가 아니다.
|
||||
4. 각 data-plane capability는 exact representation, start/end range, method,
|
||||
response status/header/length와 expiry를 묶고 한 번만 사용한다.
|
||||
5. checkpoint는 비권한성 recovery metadata만 account-partitioned storage에
|
||||
보관한다. URL, signed query/header, raw ETag, bearer token과 file path는
|
||||
저장하지 않는다.
|
||||
6. destination은 seek/truncate 가능한 명시적 port 또는 owned OPFS staging이다.
|
||||
순차 writable에 검증되지 않은 partial bytes를 append하지 않는다.
|
||||
7. checkpoint offset은 destination segment가 durable하게 commit되고 exact length가
|
||||
재확인된 뒤에만 CAS로 전진한다.
|
||||
8. final success는 destination 전체를 처음부터 다시 읽어 whole-object SHA-256을
|
||||
검증하고 final commit을 마친 경우만 `SAVED_VERIFIED`다.
|
||||
9. browser-managed handoff는 탭 종료 뒤 계속될 수 있는 기본 server-file
|
||||
fallback이지만 결과는 계속 `BROWSER_HANDOFF`다.
|
||||
10. app-managed background download는 cross-browser baseline이 아니다. 별도
|
||||
optional protocol, platform probe, worker control plane과 owned staging이 모두
|
||||
승인된 환경에서만 progressive enhancement로 조합한다.
|
||||
11. browser 차이는 user-agent 문자열이 아니라 capability probe와 정책으로
|
||||
결정한다.
|
||||
|
||||
## 4. Topology와 책임
|
||||
|
||||
```text
|
||||
product download use case
|
||||
-> product-owned download facade
|
||||
-> DownloadStrategySelector
|
||||
-> WHOLE_OBJECT_PICKER_STREAM
|
||||
-> RANGE_RESUMABLE_FOREGROUND
|
||||
-> BROWSER_MANAGED_HANDOFF
|
||||
-> BOUNDED_OBJECT_URL
|
||||
-> APP_MANAGED_BACKGROUND_DOWNLOAD (optional)
|
||||
|
||||
RANGE_RESUMABLE_FOREGROUND
|
||||
-> BFF control plane
|
||||
authorization
|
||||
immutable representation lookup
|
||||
range capability issuance/reissue
|
||||
-> browser RangeDownloadRuntime
|
||||
checkpoint + mutation lock
|
||||
exact HTTP state machine
|
||||
seekable destination or OPFS staging
|
||||
whole-object verification
|
||||
-> object store/BFF byte plane
|
||||
|
||||
APP_MANAGED_BACKGROUND_DOWNLOAD
|
||||
-> window-owned admission and user intent
|
||||
-> worker-specific control plane
|
||||
-> owned OPFS staging
|
||||
-> later foreground export
|
||||
```
|
||||
|
||||
브라우저는 bucket, object key, provider generation locator, signing key 또는 cloud
|
||||
credential을 소유하지 않는다. BFF가 logical resource를 exact immutable
|
||||
representation에 binding한다. direct object-store Range가 해당 binding과
|
||||
capability의 `preconditionMode`가 선택한 exact `If-Range` 또는
|
||||
immutable-generation precondition을 실제로 강제하지 못하면 BFF proxy/relay를
|
||||
사용한다.
|
||||
|
||||
## 5. Versioned Range capability
|
||||
|
||||
### 5.1 Application-visible handle
|
||||
|
||||
application에는 raw URL이나 validator를 노출하지 않는다.
|
||||
|
||||
```text
|
||||
RangeDownloadCapability
|
||||
protocol = RANGE_RESUMABLE_DOWNLOAD_V1
|
||||
opaque identity
|
||||
safe receipt
|
||||
resourceId
|
||||
representationBindingSha256
|
||||
totalByteLength
|
||||
mediaType
|
||||
wholeObjectSha256
|
||||
requestedStart
|
||||
requestedEndExclusive
|
||||
preconditionMode = STRONG_IF_RANGE | IMMUTABLE_GENERATION_PRECONDITION
|
||||
allowWholeObjectFallback
|
||||
expiresAtEpochMs
|
||||
```
|
||||
|
||||
adapter-owned identity vault에는 다음 data-plane binding을 함께 둔다.
|
||||
|
||||
```text
|
||||
exact HTTPS URL/query
|
||||
exact GET method
|
||||
exact origin/path
|
||||
exact Range header
|
||||
exact precondition header/value selected by preconditionMode
|
||||
required response headers
|
||||
allowed statuses = policy-derived exact subset of 200 | 206 | 412 | 416
|
||||
expected representation binding
|
||||
maximum response bytes
|
||||
single-use receipt
|
||||
```
|
||||
|
||||
`representationBindingSha256`는 protocol/version, logical resource, immutable
|
||||
generation, precondition mode별 normalized strong validator 또는 generation
|
||||
binding, exact total length, media type와 expected whole-object digest의 canonical
|
||||
binding이다. 이것은 authorization proof가 아니다. BFF는 client 값을 echo하지
|
||||
않고 registry snapshot에서 직접 재계산한다.
|
||||
|
||||
### 5.2 Strong validator
|
||||
|
||||
resume에는 다음 중 하나가 필요하다.
|
||||
|
||||
- server registry가 소유하는 immutable object generation과 그 generation에 pin된
|
||||
proxy/direct request
|
||||
- RFC semantics를 만족하는 strong ETag와 exact `If-Range`
|
||||
|
||||
weak ETag(`W/`), `Last-Modified`만 있는 representation, multipart ETag를 whole
|
||||
digest로 해석한 값과 CDN이 임의로 다시 쓴 validator는 resume authority로
|
||||
사용하지 않는다. provider가 strong validator를 제공하지 못하면 BFF가 immutable
|
||||
generation을 pin하거나 Range resume를 `UNSUPPORTED`로 닫는다.
|
||||
|
||||
raw ETag와 provider generation locator는 application, checkpoint, diagnostics와
|
||||
telemetry에 노출하지 않는다. reload 뒤에는 BFF가 새 capability를 발급하고,
|
||||
runtime은 새 capability의 `representationBindingSha256`가 checkpoint와 같은지
|
||||
확인한 뒤 vault 안의 exact precondition만 사용한다.
|
||||
|
||||
`STRONG_IF_RANGE` mode는 exact `If-Range`를 보내고 `206`, Range-ignore 또는
|
||||
validator mismatch의 full `200`과 해당 `416`만 계약한다.
|
||||
`IMMUTABLE_GENERATION_PRECONDITION` mode는 BFF/provider가 정한 exact `If-Match`
|
||||
또는 generation precondition을 보내며 `412`를 계약할 수 있다.
|
||||
`allowWholeObjectFallback`과 `allowedStatuses`는 mode, requested start와 provider
|
||||
topology에서 capability 발급 시 닫히며 executor가 임의로 넓히지 않는다.
|
||||
|
||||
### 5.3 Capability 재발급
|
||||
|
||||
capability expiry, data-plane `401/403/410` 또는 최소 잔여 lifetime 부족은 같은
|
||||
URL의 무조건 retry가 아니다.
|
||||
|
||||
1. 현재 response reader를 cancel하고 capability를 consume한다.
|
||||
2. control plane에 `downloadKey`, resource와 expected representation binding,
|
||||
exact next range를 전달한다.
|
||||
3. BFF가 authorization와 current generation을 다시 읽는다.
|
||||
4. binding이 같을 때만 새 capability로 같은 range를 재시도한다.
|
||||
5. binding이 바뀌었으면 partial destination을 append하지 않고
|
||||
`REPRESENTATION_CHANGED/RESTART`로 닫는다.
|
||||
|
||||
재발급 횟수, 전체 operation deadline과 retry backoff는 composition hard ceiling
|
||||
안에 둔다. capability를 durable queue나 worker message에 저장하지 않는다.
|
||||
|
||||
## 6. Durable checkpoint
|
||||
|
||||
### 6.1 Schema
|
||||
|
||||
```text
|
||||
RangeDownloadCheckpointV1
|
||||
schemaVersion = 1
|
||||
protocol = RANGE_RESUMABLE_DOWNLOAD_V1
|
||||
revision
|
||||
state = ACTIVE | PAUSED | FINALIZING | CLEANUP_PENDING
|
||||
downloadKey
|
||||
resourceBindingSha256
|
||||
representationBindingSha256
|
||||
totalByteLength
|
||||
nextOffset
|
||||
committedSegmentCount
|
||||
destination
|
||||
kind = OPFS_STAGING | SEEKABLE_FILE
|
||||
opaqueDestinationBinding
|
||||
createdAtEpochMs
|
||||
updatedAtEpochMs
|
||||
retentionExpiresAtEpochMs
|
||||
```
|
||||
|
||||
`downloadKey`, destination binding과 physical database/OPFS namespace는
|
||||
composition-issued opaque token이다. 사용자 file name, resource ID, account ID,
|
||||
tenant ID 또는 local path를 넣지 않는다.
|
||||
|
||||
checkpoint에 금지하는 값:
|
||||
|
||||
- presigned URL, query와 signed request/response header
|
||||
- bearer/session/auth/CSRF token
|
||||
- raw ETag, provider object key/generation locator
|
||||
- file name, user path와 native exception
|
||||
- incremental hash 내부 state
|
||||
- raw backend response나 retry body
|
||||
|
||||
허용된 digest binding과 offset은 비권한성 recovery metadata다. account partition,
|
||||
retention, count/byte budget과 logout deletion을 적용하며 log/analytics/ticket에는
|
||||
내보내지 않는다.
|
||||
|
||||
### 6.2 CAS와 durable offset
|
||||
|
||||
한 `downloadKey`는 cross-context exclusive mutation lock으로 직렬화한다. lock은
|
||||
correctness의 유일한 authority가 아니며 checkpoint revision CAS와 exact
|
||||
destination binding이 최종 local authority다.
|
||||
|
||||
`nextOffset`은 다음 순서가 모두 성공한 뒤에만 전진한다.
|
||||
|
||||
1. exact `206` range를 bounded stream으로 읽는다.
|
||||
2. expected start 위치에만 쓴다.
|
||||
3. writer close/segment commit을 완료한다.
|
||||
4. destination의 committed length가 expected end 이상인지 확인한다.
|
||||
5. unexpected tail이 있으면 authorized `truncate(expectedEnd)`를 완료한다.
|
||||
6. checkpoint를 `revision + 1`, `nextOffset = expectedEnd`로 CAS한다.
|
||||
|
||||
response가 성공했지만 destination commit 전에 crash하면 checkpoint는 이전
|
||||
offset에 머문다. 재시작은 destination을 checkpoint offset으로 truncate하고 같은
|
||||
range를 다시 요청한다. destination commit 뒤 checkpoint CAS가 유실된 경우도
|
||||
동일하게 checkpoint offset까지 truncate한 뒤 재전송한다. 따라서 중복 byte를
|
||||
append하지 않는다.
|
||||
|
||||
### 6.3 Inventory와 retention
|
||||
|
||||
checkpoint store는 단일 key read 외에 bounded admin operation을 제공해야 한다.
|
||||
|
||||
- account partition 안의 safe summary를 cursor page로 list
|
||||
- expired/terminal checkpoint를 bounded batch로 classify
|
||||
- destination binding과 함께 exact owned staging을 cleanup
|
||||
- active lock/lease가 있는 항목은 건너뜀
|
||||
- count, logical bytes, maximum age와 cleanup retry budget 강제
|
||||
- cleanup receipt를 durable하게 남기고 response 유실을 reconcile
|
||||
|
||||
inventory에는 resource ID, file name, digest, raw validator와 path를 반환하지
|
||||
않는다. 제품 resume UI가 필요한 경우 제품 database/query가 별도 safe display
|
||||
metadata를 소유하고 opaque `downloadKey`로만 연결한다.
|
||||
|
||||
## 7. Destination 계약
|
||||
|
||||
### 7.1 공통 port
|
||||
|
||||
```text
|
||||
ResumableDownloadDestinationPort
|
||||
inspect(binding) -> committedLength, readable, writable, permissionState
|
||||
openWriter(binding, keepExistingData=true)
|
||||
seek(offset)
|
||||
write(chunk)
|
||||
truncate(length)
|
||||
commitSegment()
|
||||
openReader(start=0)
|
||||
finalize()
|
||||
abortAttempt()
|
||||
cleanup(authority)
|
||||
```
|
||||
|
||||
native handle, OPFS handle와 path는 adapter 밖으로 노출하지 않는다. 모든 method는
|
||||
bounded deadline, AbortSignal과 closed failure를 사용한다.
|
||||
|
||||
### 7.2 Seekable external file
|
||||
|
||||
직접 외부 파일에 resume하려면 browser가 기존 data 보존, seek, truncate,
|
||||
재읽기와 permission 재확인을 실제로 지원해야 한다.
|
||||
|
||||
- picker와 permission request는 Window의 명시적 user activation에서만 실행한다.
|
||||
- structured-cloned handle을 보존하는 경우 별도 privacy/retention 승인이 필요하다.
|
||||
- reopen 뒤 `queryPermission`/`requestPermission`을 거치며 denied면
|
||||
`PERMISSION_DENIED/RESELECT`다.
|
||||
- writer가 temporary-file commit semantics를 쓰면 segment마다 close한 뒤
|
||||
committed file size를 다시 확인한다.
|
||||
- checkpoint보다 큰 tail은 검증하지 않고 사용하지 않으며 exact checkpoint
|
||||
offset으로 truncate한다.
|
||||
- checkpoint보다 파일이 작거나 다른 handle이면 `CONFLICT/RESTART`다.
|
||||
|
||||
브라우저가 이 계약을 만족하지 못하면 external-file resume를 흉내 내지 않고 OPFS
|
||||
staging 또는 browser-managed handoff로 전환한다.
|
||||
|
||||
### 7.3 OPFS staging
|
||||
|
||||
cross-browser app-controlled resume의 우선 destination은 policy-owned OPFS
|
||||
staging이다.
|
||||
|
||||
- physical path는 기존 OPFS authority/namespace/partition registry가 발급한다.
|
||||
- checkpoint와 OPFS object는 immutable binding과 generation journal로 연결한다.
|
||||
- quota estimate는 admission hint일 뿐이며 write 중 quota failure도 처리한다.
|
||||
- download 완료 뒤 staging 전체를 다시 읽어 SHA-256을 검증한다.
|
||||
- foreground user activation에서 새 외부 destination을 열고 staging을 stream
|
||||
export한다.
|
||||
- 외부 export close가 성공하기 전 staging을 삭제하지 않는다.
|
||||
- export 결과가 유실되면 staging을 유지하고 user에게 retry 가능한 상태를
|
||||
반환한다.
|
||||
|
||||
OPFS 저장 성공은 사용자가 접근 가능한 파일 저장 완료가 아니다. 결과를
|
||||
`STAGED_VERIFIED`와 `SAVED_VERIFIED`로 구분한다. OPFS는 큰 파일에서 storage와
|
||||
I/O를 한 번 더 요구하므로 quota/retention owner 없는 기본 fallback이 아니다.
|
||||
|
||||
## 8. HTTP 상태 머신
|
||||
|
||||
### 8.1 요청 전
|
||||
|
||||
1. checkpoint와 destination binding을 exact하게 읽는다.
|
||||
2. destination length를 검사하고 checkpoint보다 큰 tail을 truncate한다.
|
||||
3. checkpoint보다 작으면 partial을 신뢰하지 않고 restart/cleanup으로 닫는다.
|
||||
4. 새 capability의 representation binding과 exact range를 검증한다.
|
||||
5. `Range: bytes=S-E`와 capability의 `preconditionMode`가 정한 exact
|
||||
`If-Range` 또는 immutable-generation precondition을 vault binding 그대로
|
||||
보낸다.
|
||||
6. `credentials: omit`, `redirect: error`, `no-referrer`, `no-store`,
|
||||
identity content encoding을 강제한다.
|
||||
|
||||
한 request의 range 크기와 exact `S/E`는 capability 발급 **전에** composition
|
||||
maximum 안에서 계산한다. executor는 capability의 `requestedStart`,
|
||||
`requestedEndExclusive`와 exact Range header가 일치하는지 검증하고 그대로
|
||||
전송하며 다시 줄이거나 늘리지 않는다. ceiling을 넘는 capability는 사용 전에
|
||||
거절한다. 기본 protocol은 sequential range만 허용한다. parallel range와 sparse
|
||||
destination은 별도 protocol/version 없이는 사용하지 않는다.
|
||||
zero-byte representation은 유효하지 않은 byte range를 만들지 않는다. exact
|
||||
length가 0이고 empty-object SHA-256 binding이 일치하는 whole-object `200` 경로로
|
||||
body/length를 확인한 뒤 바로 final verification으로 이동한다.
|
||||
|
||||
response body를 읽거나 destination writer를 열기 전에 `200`, `206`, `412`,
|
||||
`416` 중 수신한 status가 capability vault의 exact `allowedStatuses` member인지
|
||||
검사한다. 해당 네 값 중 허용되지 않은 status는 body를 cancel하고 capability를
|
||||
consume하며 destination과 checkpoint를 변경하지 않은 채
|
||||
`CONTRACT_MISMATCH`로 fail-closed한다. 아래 네 분기는 이 공통 admission gate를
|
||||
통과한 경우에만 실행한다. 그 밖의 status는 §8.6의 별도 failure/reissue 규칙으로
|
||||
처리한다.
|
||||
|
||||
### 8.2 `206 Partial Content`
|
||||
|
||||
성공 조건:
|
||||
|
||||
- `206`이 capability의 `allowedStatuses` member
|
||||
- final response URL이 capability URL과 exact match
|
||||
- `Content-Range: bytes S-E/T`가 하나만 존재하고 parse가 엄격함
|
||||
- `S`가 requested start, `E + 1`이 requested end exclusive
|
||||
- `T`가 checkpoint total과 같음
|
||||
- `Content-Length = E - S + 1`
|
||||
- strong validator/immutable generation binding 일치
|
||||
- media type과 identity encoding 일치
|
||||
- body 실제 bytes가 exact content length
|
||||
|
||||
하나라도 다르면 reader와 current destination attempt를 abort하고 checkpoint를
|
||||
전진시키지 않는다. 정상인 경우에만 앞 절의 durable offset 순서로 commit한다.
|
||||
|
||||
### 8.3 `200 OK`
|
||||
|
||||
`200`은 capability의 `allowedStatuses` member인 경우에만 이 분기로 들어온다.
|
||||
body를 destination에 쓰기 전에 final response URL, required response header,
|
||||
media type, identity encoding과 mode별 strong validator 또는 immutable-generation
|
||||
evidence가 capability의 exact representation binding과 일치하는지 검증한다.
|
||||
|
||||
다음 순서로 배타적으로 처리한다.
|
||||
|
||||
1. validator/generation evidence가 없거나 binding이 다르면 body를 cancel하고
|
||||
capability를 consume한다. 기존 checkpoint와 partial은 append하지 않고
|
||||
quarantine/retention policy로 전환한 뒤 control plane에서 current
|
||||
representation을 다시 확인한다. 결과는
|
||||
`REPRESENTATION_CHANGED/RESTART`이며 byte 0의 새 operation만 허용한다.
|
||||
2. binding은 같지만 requested start가 `0`이고
|
||||
`allowWholeObjectFallback=true`이면 fresh whole-object destination에서 기존
|
||||
whole-object stream 계약으로 처리한다. exact total length와 final
|
||||
whole-object digest를 검증하기 전에는 success나 final commit을 반환하지 않는다.
|
||||
3. binding은 같고 requested start가 `0`이지만
|
||||
`allowWholeObjectFallback=false`이면 body를 한 byte도 쓰지 않고 cancel한다.
|
||||
결과는 `WHOLE_OBJECT_FALLBACK_NOT_ALLOWED`이며 policy가 허용한 새 Range
|
||||
capability, browser handoff 또는 explicit unsupported만 선택한다.
|
||||
4. binding은 같고 requested start가 `0`보다 크면 server가 Range를 무시한
|
||||
것이다. body를 한 byte도 쓰지 않고 cancel하며 기존 partial을 같은 writer에서
|
||||
덮어쓰지 않는다. control plane reconcile 뒤 같은 representation의 byte 0
|
||||
restart operation, browser handoff 또는 explicit unsupported만 선택한다.
|
||||
|
||||
### 8.4 `412 Precondition Failed`
|
||||
|
||||
`412`가 capability의 `allowedStatuses` member이고
|
||||
`preconditionMode=IMMUTABLE_GENERATION_PRECONDITION`인 경우에만 이 분기로 들어온다.
|
||||
representation precondition 실패다. body를 cancel하고 checkpoint를 유지한 채
|
||||
control plane에서 current generation을 확인한다. 같은 binding을 다시 발급하지
|
||||
못하면 partial은 cleanup policy에 따라 폐기하고 byte 0부터 새 operation을
|
||||
시작한다.
|
||||
|
||||
RFC `If-Range` validator mismatch 자체의 정상 응답은 `200`이다. `412`는 BFF나
|
||||
provider가 immutable generation을 pin하기 위해 별도 `If-Match` 계열 precondition을
|
||||
함께 강제하는 topology에서만 이 상태 머신에 들어온다. topology가 `412`를 계약하지
|
||||
않았다면 unknown status로 fail-closed한다.
|
||||
|
||||
### 8.5 `416 Range Not Satisfiable`
|
||||
|
||||
`416`이 capability의 `allowedStatuses` member인 경우에만 이 분기로 들어온다.
|
||||
response body는 download data로 소비하지 않고 cancel한다.
|
||||
`Content-Range: bytes */T`를 strict하게 검사하며 final response URL, required
|
||||
headers와 mode별 validator/generation binding도 확인한다. provider의 `416`이
|
||||
binding evidence를 반환할 수 없는 topology라면 BFF control plane reconcile이
|
||||
exact immutable generation을 다시 증명하기 전에는 EOF나 missing-range 분기로
|
||||
진행하지 않는다.
|
||||
|
||||
다음 순서를 사용하며 한 분기를 처리한 뒤 아래 분기로 fall through하지 않는다.
|
||||
|
||||
1. malformed/missing `T`, final URL/header mismatch 또는 증명되지 않은
|
||||
representation binding은 `CONTRACT_MISMATCH`로 fail-closed한다.
|
||||
2. `T != expected total`이면 representation changed다. local bytes를 `T`에 맞춰
|
||||
자동 truncate하거나 append하지 않고 capability를 consume한 뒤 partial을
|
||||
quarantine/restart한다.
|
||||
3. `nextOffset > T`이면 checkpoint 자체가 corrupt/stale이다. 잘못된 offset으로
|
||||
truncate하지 않고 checkpoint와 partial을 quarantine한 뒤 restart/recovery로
|
||||
닫는다.
|
||||
4. `nextOffset <= T`이지만 `local committed length != nextOffset`이면 먼저 local
|
||||
state를 reconcile한다.
|
||||
- local length가 더 크면 exact `nextOffset`까지만 uncommitted tail을
|
||||
authorized truncate하고 durable length를 다시 확인한다.
|
||||
- local length가 더 작으면 journal이 증명하는 마지막 confirmed segment로
|
||||
destination과 checkpoint를 함께 CAS rollback할 수 있을 때만 복구한다.
|
||||
그렇지 않으면 quarantine/restart한다.
|
||||
이 분기는 reconcile 결과를 새 state-machine invocation에서 다시 평가하며 바로
|
||||
finalization이나 missing-range request로 진행하지 않는다.
|
||||
5. `local committed length == nextOffset == T`이면 data transfer가 끝난 후보로
|
||||
보고 `FINALIZING` whole-object verification으로 이동한다.
|
||||
6. `local committed length == nextOffset < T`이면 local missing range가 남아 있다.
|
||||
새 capability로 exact `nextOffset` range를 재발급한다. 같은 total에 대해
|
||||
satisfiable range가 다시 `416`이면 bounded retry하지 않고
|
||||
`CONTRACT_MISMATCH`로 fail-closed한다.
|
||||
|
||||
`416` 자체를 다운로드 성공으로 간주하지 않는다.
|
||||
|
||||
### 8.6 나머지 상태와 network failure
|
||||
|
||||
| 조건 | 처리 |
|
||||
| --- | --- |
|
||||
| `401/403/410` | bounded capability reissue; binding mismatch면 restart |
|
||||
| `404` | existence-hiding policy에 따라 unavailable/not-found, partial cleanup 예약 |
|
||||
| `409` | server representation/session reconcile |
|
||||
| `429`/모든 `5xx`/network | 동일 exact range만 bounded retry |
|
||||
| redirect/opaque response | policy rejection |
|
||||
| timeout/cancel | reader와 writer attempt abort, checkpoint 유지 |
|
||||
| overrun/truncation | integrity failure, checkpoint 유지 |
|
||||
|
||||
retry는 destination commit 여부를 먼저 판단한다. effect가 ambiguous하면
|
||||
checkpoint와 destination length를 reconcile하기 전 새 offset으로 이동하지 않는다.
|
||||
|
||||
## 9. Whole-object integrity와 final commit
|
||||
|
||||
Range별 transport 검증은 whole-object 무결성 증거가 아니다. 모든 bytes가
|
||||
수신되면 checkpoint를 `FINALIZING`으로 CAS하고 다음을 수행한다.
|
||||
|
||||
1. destination length가 exact total과 같은지 확인한다.
|
||||
2. destination을 byte 0부터 bounded chunk로 다시 읽는다.
|
||||
3. vetted incremental SHA-256으로 whole-object digest를 계산한다.
|
||||
4. capability/representation binding의 expected digest와 constant-time 비교한다.
|
||||
5. mismatch면 사용자 destination을 성공으로 표시하지 않고 staging을 격리하거나
|
||||
authorized cleanup한다.
|
||||
6. OPFS staging이면 foreground external export와 destination close를 완료한다.
|
||||
7. final destination commit truth를 확인한 뒤만 `SAVED_VERIFIED`를 반환한다.
|
||||
8. checkpoint와 staging cleanup을 exact revision/receipt로 완료한다.
|
||||
|
||||
portable하지 않은 incremental hash 내부 state를 checkpoint에 serialize하지 않는다.
|
||||
마지막 full reread 비용을 피하려면 chunk digest/Merkle manifest를 별도 protocol로
|
||||
설계하고 server가 exact proof를 제공해야 한다.
|
||||
|
||||
## 10. Pause, cancel, crash와 account lifecycle
|
||||
|
||||
### 10.1 Pause
|
||||
|
||||
`pause(downloadKey)`는 browser work 중단이며 server resource/capability revoke가
|
||||
아니다.
|
||||
|
||||
- 같은 runtime의 read/write/backoff를 AbortSignal로 중단한다.
|
||||
- same-origin context에는 opaque key만 담은 versioned ephemeral pause event를
|
||||
보낸다.
|
||||
- mutation lock 안에서 checkpoint를 `PAUSED`로 CAS한다.
|
||||
- in-memory URL/header/capability는 즉시 retire한다.
|
||||
- committed segment는 유지하고 ambiguous writer attempt는 checkpoint offset으로
|
||||
reconcile한다.
|
||||
|
||||
### 10.2 Cancel과 discard
|
||||
|
||||
cancel은 transfer 중단만 의미할 수 있고, discard는 local partial 삭제다. 제품
|
||||
facade가 두 의도를 구분해야 한다. discard는 exact partition/destination binding과
|
||||
short-lived maintenance authority를 요구하며 checkpoint와 OPFS staging을 하나의
|
||||
cleanup journal로 처리한다.
|
||||
|
||||
### 10.3 Crash/reload
|
||||
|
||||
reload 후 runtime은:
|
||||
|
||||
1. account partition과 governance binding을 검증한다.
|
||||
2. checkpoint schema/protocol/revision을 검증한다.
|
||||
3. destination을 reopen하고 permission/length를 검사한다.
|
||||
4. server에서 새 capability를 발급받아 representation binding을 대조한다.
|
||||
5. exact checkpoint offset부터 resume한다.
|
||||
|
||||
source가 같은지 사용자에게 묻는 file-name 기반 확인은 사용하지 않는다.
|
||||
|
||||
### 10.4 Logout/account/tenant switch
|
||||
|
||||
- 신규 capability 발급과 resume admission을 먼저 닫는다.
|
||||
- active foreground operation을 abort하고 writer를 정리한다.
|
||||
- vault와 worker channel을 close한다.
|
||||
- account partition의 checkpoint와 owned staging을 maintenance-authorized bounded
|
||||
cleanup으로 제거한다.
|
||||
- blocked deletion을 성공으로 보고하지 않는다.
|
||||
- 이전 account handle/reference를 새 runtime에서 resolve하지 않는다.
|
||||
|
||||
retention/legal-hold 정책이 local partial 보존을 요구하는 특별한 제품이 아니라면
|
||||
logout에서 partial을 제거하는 것이 기본이다.
|
||||
|
||||
## 11. Download strategy selector
|
||||
|
||||
selector는 presentation의 임의 조건문이 아니라 composition-owned immutable
|
||||
policy와 runtime probe를 받는 공통 application service다.
|
||||
|
||||
입력:
|
||||
|
||||
- source가 server resource인지 client-generated artifact인지
|
||||
- exact 또는 maximum byte length
|
||||
- verified integrity 필요 여부
|
||||
- resume/background 요구
|
||||
- system save picker, seek/truncate, OPFS와 worker capability
|
||||
- user activation
|
||||
- storage quota admission
|
||||
- browser-managed capability availability
|
||||
- data classification와 retention policy
|
||||
|
||||
결과:
|
||||
|
||||
| 조건 | 선택 |
|
||||
| --- | --- |
|
||||
| server file, 탭 종료 뒤 계속 필요 | `BROWSER_MANAGED_HANDOFF` |
|
||||
| server file, verified foreground save, picker 지원 | `WHOLE_OBJECT_PICKER_STREAM` |
|
||||
| server file, resume 필수, destination 계약 충족 | `RANGE_RESUMABLE_FOREGROUND` |
|
||||
| 작은 generated artifact | `BOUNDED_OBJECT_URL` |
|
||||
| 큰 generated artifact, picker 지원 | `WHOLE_OBJECT_PICKER_STREAM` |
|
||||
| 큰 generated artifact, picker 미지원 | `SERVER_GENERATION_REQUIRED` 또는 `UNSUPPORTED` |
|
||||
| app background download가 승인·지원되고 OPFS quota 확보 | `APP_MANAGED_BACKGROUND_DOWNLOAD` |
|
||||
|
||||
selector는 fallback으로 byte/memory/security ceiling을 올리지 않는다. integrity가
|
||||
필수인데 browser handoff만 가능하면 “검증된 저장”으로 downgrade하지 않고 제품이
|
||||
handoff 또는 unsupported 중 하나를 명시적으로 선택한다.
|
||||
|
||||
### Safari와 picker 미지원 환경
|
||||
|
||||
user-agent 문자열로 Safari를 판별하지 않는다. 필요한 API와 실제 semantics를
|
||||
capability probe로 확인한다.
|
||||
|
||||
- 대용량 server file: authorized `Content-Disposition` browser handoff
|
||||
- 작은 generated file: bounded Blob/object URL
|
||||
- 대용량 generated file: server-side generation 또는 unsupported
|
||||
- OPFS: app-private staging일 뿐 Finder/Files 저장 완료로 표시하지 않음
|
||||
- system save picker 미지원: unbounded Blob으로 자동 전환하지 않음
|
||||
- seek/truncate/permission semantics 미충족: external Range resume 비활성화
|
||||
|
||||
browser-managed handoff endpoint는 cross-origin `download` attribute에 의존하지
|
||||
않고 server가 safe `Content-Disposition`, media type, byte/generation policy를
|
||||
실제 response에서 강제한다.
|
||||
|
||||
## 12. Background download 전달의 세 의미
|
||||
|
||||
### 12.1 Foreground app-managed
|
||||
|
||||
page가 열린 동안 runtime이 fetch, progress, integrity와 destination을 모두
|
||||
관리한다. 현재 whole-object stream과 목표 Range resume가 이 범주다. page lifecycle
|
||||
종료 뒤 지속을 보장하지 않는다.
|
||||
|
||||
### 12.2 Browser-managed handoff
|
||||
|
||||
navigation/download manager에 authorized endpoint를 넘긴다.
|
||||
|
||||
- page 종료 뒤 계속될 수 있는 가장 넓은 fallback
|
||||
- application은 실제 disk write, 저장 위치와 final digest를 관찰하지 못함
|
||||
- 결과는 `BROWSER_HANDOFF`, `SAVED`나 `VERIFIED`가 아님
|
||||
- pause/resume UI와 retry semantics는 browser가 소유
|
||||
|
||||
### 12.3 App-managed background download
|
||||
|
||||
Service Worker/Background Fetch 등에서 application이 progress/retry/staging을
|
||||
관리하려는 별도 optional capability다.
|
||||
|
||||
필수 조건:
|
||||
|
||||
- target browser/deployment의 explicit support matrix
|
||||
- worker-safe authenticated control plane
|
||||
- worker가 매 range마다 새 short-lived capability를 발급받는 계약
|
||||
- capability/URL/header를 IDB, OPFS, Cache Storage와 message에 저장하지 않음
|
||||
- private bytes는 Cache Storage가 아니라 policy-owned OPFS staging 사용
|
||||
- worker termination을 정상 상태로 보고 checkpoint에서 재개
|
||||
- concurrency, battery/network, quota와 retention ceiling
|
||||
- logout/revocation event와 worker admission fence
|
||||
- client/worker version compatibility와 upgrade drain
|
||||
- notification/foreground export UX
|
||||
|
||||
일반 Service Worker의 수명이나 background execution 시간을 correctness 근거로
|
||||
삼지 않는다. Background Fetch가 없는 환경에서 timer/keepalive로 장기 download를
|
||||
흉내 내지 않는다. user-visible external save picker는 worker에서 호출하지 않고
|
||||
완료된 OPFS staging을 다음 foreground user gesture에서 export한다.
|
||||
|
||||
따라서 app-managed background download가 향후 `AVAILABLE_NOT_COMPOSED` 또는 `COMPOSED`가
|
||||
되더라도 지원 browser의 progressive enhancement일 뿐이다. cross-browser 보장
|
||||
자체의 primary status는 계속 `PLATFORM_LIMITED`다.
|
||||
|
||||
## 13. Security, privacy와 observability
|
||||
|
||||
- URL/query/header, validator와 capability는 bearer 또는 sensitive metadata로
|
||||
취급한다.
|
||||
- `Range`와 `preconditionMode`가 선택한 exact `If-Range` 또는
|
||||
immutable-generation precondition은 adapter vault가 binding 그대로 생성한다.
|
||||
- caller는 offset을 늘리거나 arbitrary range를 요청하지 못한다.
|
||||
- account partition과 resource authorization을 매 capability reissue에서 검사한다.
|
||||
- partial bytes는 원본과 같은 data classification, retention, encryption-at-rest와
|
||||
deletion policy를 적용한다.
|
||||
- OPFS quota pressure가 다른 account partial을 제거할 권한을 주지 않는다.
|
||||
- preview, execution 또는 Cache Storage promotion은 final verification 전 금지한다.
|
||||
- high-cardinality ID, file name, path, URL, raw ETag와 digest를 metric label/log에
|
||||
넣지 않는다.
|
||||
|
||||
허용된 aggregate observation:
|
||||
|
||||
- strategy와 destination kind
|
||||
- response state bucket
|
||||
- expected/committed byte bucket
|
||||
- retry/reissue/resume count bucket
|
||||
- duration, pause, restart, integrity와 cleanup outcome
|
||||
- browser capability support reason code
|
||||
|
||||
## 14. Composition과 operational admission
|
||||
|
||||
`createBrowserTransferRuntime`에 해당하는 미래 composition owner만 다음을 조합한다.
|
||||
|
||||
- versioned wire codecs와 fixed BFF endpoint
|
||||
- capability vault/provider/executor
|
||||
- Range checkpoint store, destination registry와 mutation lock
|
||||
- selector policy와 browser capability probe
|
||||
- presigned, upload, image와 Range lifecycle
|
||||
- account/logout cleanup authority
|
||||
- safe observer
|
||||
- traffic admission과 kill switch
|
||||
|
||||
독립적인 canonical readiness 상태:
|
||||
|
||||
```text
|
||||
Selection = NOT_SELECTED | SELECTED | REMOVING
|
||||
TrafficAdmission = DISABLED | SHADOW | CANARY | ENABLED
|
||||
RuntimeHealth = UNKNOWN | AVAILABLE | DEGRADED | UNAVAILABLE | INCOMPATIBLE
|
||||
PromotionEvidence = MISSING | PARTIAL | COMPLETE | EXPIRED
|
||||
```
|
||||
|
||||
primary status가 `COMPOSED`여도 `TrafficAdmission` 기본값은 `DISABLED`다.
|
||||
필수 config, strong validator/provider conformance, destination semantics, cleanup
|
||||
owner 또는 valid evidence가 없으면 `TrafficAdmission=DISABLED`,
|
||||
`RuntimeHealth=UNKNOWN | UNAVAILABLE`,
|
||||
`PromotionEvidence=MISSING | PARTIAL | EXPIRED`로 readiness를 fail-closed한다.
|
||||
이미 승인된 product selection 자체를 provider evidence 부족만으로 되돌리지
|
||||
않는다.
|
||||
|
||||
Kill switch:
|
||||
|
||||
- 신규 Range capability issuance off
|
||||
- Range resume off → whole-object restart 또는 browser handoff
|
||||
- direct object-store Range off → BFF proxy
|
||||
- external seek destination off → OPFS staging 또는 handoff
|
||||
- app-managed background download off → foreground/browser handoff
|
||||
- final export off → verified staging 유지
|
||||
|
||||
kill switch는 partial을 자동 삭제하거나 handoff를 saved/verified로 바꾸지 않는다.
|
||||
|
||||
## 15. Test와 conformance matrix
|
||||
|
||||
### 15.1 Deterministic runtime
|
||||
|
||||
- checkpoint CAS conflict와 corrupt/unknown field
|
||||
- exact segment commit 전/후 crash
|
||||
- destination larger/smaller/different binding
|
||||
- pause/resume/cancel/discard races
|
||||
- capability expiry/reissue와 representation change
|
||||
- `200/206/412/416` 모든 분기
|
||||
- malformed/multiple/overflow `Content-Range`
|
||||
- weak/missing/mismatched validator
|
||||
- overrun, truncation, stalled body와 abort
|
||||
- final whole-object digest mismatch
|
||||
- cleanup response loss와 replay
|
||||
- count/byte/age retention sweep
|
||||
|
||||
### 15.2 Browser matrix
|
||||
|
||||
- system picker 지원/미지원
|
||||
- seek/truncate/keep-existing-data semantics
|
||||
- OPFS quota, eviction, reload와 worker termination
|
||||
- cross-tab lock/pause delivery
|
||||
- user activation과 permission denied/revoked
|
||||
- large server handoff
|
||||
- foreground export close/abort
|
||||
- Chromium, Firefox와 WebKit 동일 필수 case set
|
||||
|
||||
지원하지 않는 API는 skip이 아니라 selector의 expected fallback/`UNSUPPORTED` 결과로
|
||||
검증한다.
|
||||
|
||||
### 15.3 BFF/object provider contract
|
||||
|
||||
- immutable generation pin
|
||||
- capability `preconditionMode`에 따른 strong `If-Range` 또는 immutable
|
||||
generation precondition
|
||||
- beginning/middle/end/empty/invalid range
|
||||
- exact `206 Content-Range`와 length
|
||||
- deliberate Range ignore `200`
|
||||
- mode가 계약한 경우의 precondition `412`, EOF/invalid `416`
|
||||
- mid-transfer capability expiry/revocation
|
||||
- redirect/CORS/exposed-header/identity-encoding
|
||||
- object replacement race
|
||||
- direct provider와 proxy 결과 동등성
|
||||
- URL/header/log redaction
|
||||
|
||||
fake와 route interception은 actual provider conformance를 대체하지 않는다.
|
||||
|
||||
### 15.4 Background-download-specific fault
|
||||
|
||||
- worker가 range commit 전/후 종료
|
||||
- worker/client version 교체
|
||||
- logout과 capability revocation
|
||||
- offline/online 반복, quota exhaustion과 battery/network policy
|
||||
- notification 유실과 foreground export replay
|
||||
- unsupported browser가 foreground/handoff로 정확히 fallback
|
||||
|
||||
## 16. Rollout과 promotion
|
||||
|
||||
Primary status 변경과 readiness/traffic promotion은 별도로 승인한다.
|
||||
|
||||
1. Range의 `DESIGNED_NOT_IMPLEMENTED`와 ADR-local
|
||||
`sourceEvidence=DESIGN_REVIEWED`를 확인한다.
|
||||
2. provider-neutral ports/runtime, deterministic fake, negative fixture와 browser
|
||||
test를 완성한 경우에만 Range primary status를
|
||||
`AVAILABLE_NOT_COMPOSED`, source evidence를 `REFERENCE_TESTED`로 변경한다.
|
||||
deterministic/reference test만으로 canonical `PromotionEvidence`를
|
||||
`COMPLETE`로 바꾸지 않는다.
|
||||
3. 제품 요구, owner, data class와 fallback이 선택되지 않은 app-managed
|
||||
background download는 계속 `NOT_SELECTED`로 둔다. cross-browser 보장은
|
||||
`PLATFORM_LIMITED`다.
|
||||
4. fixed staging BFF/provider, actual config, account lifecycle와 runbook을 설치한
|
||||
capability만 `COMPOSED`로 기록한다. 이때도
|
||||
`TrafficAdmission=DISABLED`, `RuntimeHealth=UNKNOWN`,
|
||||
`PromotionEvidence=PARTIAL`이다.
|
||||
5. operator probe와 shadow에서 contract evidence를 수집한다.
|
||||
6. internal cohort에서 BFF proxy Range를 먼저 canary한다.
|
||||
7. direct provider Range와 external seek destination은 각각 별도 canary한다.
|
||||
8. app-managed background download를 실제로 선택했다면 지원 browser cohort에서만 별도
|
||||
opt-in canary한다.
|
||||
9. contract/provider/browser/operations component gate, SLO, cleanup drill,
|
||||
rollback과 evidence freshness가 모두 충족된 승인 범위만
|
||||
`PromotionEvidence=COMPLETE`, `RuntimeHealth=AVAILABLE`,
|
||||
`TrafficAdmission=ENABLED`로 promotion한다.
|
||||
|
||||
provider, endpoint, validator semantics, browser major behavior, destination adapter,
|
||||
wire protocol 또는 security policy가 바뀌면 relevant evidence를 만료시키고
|
||||
재승인한다.
|
||||
|
||||
## 17. Rollback과 제거
|
||||
|
||||
운영 rollback 순서:
|
||||
|
||||
1. 신규 Range/background-download admission과 capability 발급을 중지한다.
|
||||
2. active writer/worker를 abort하고 checkpoint offset으로 reconcile한다.
|
||||
3. app background download를 foreground/browser handoff로 낮춘다.
|
||||
4. direct Range를 BFF proxy 또는 whole-object restart로 낮춘다.
|
||||
5. verified OPFS staging은 retention window 안에서 foreground export 가능 상태로
|
||||
유지한다.
|
||||
6. ambiguous partial은 성공으로 표시하지 않고 cleanup queue로 넘긴다.
|
||||
7. provider/signing credential 노출이 원인이면 backend revoke와 key rotation을
|
||||
수행한다.
|
||||
|
||||
완전 제거:
|
||||
|
||||
1. pending checkpoint/staging inventory를 bounded하게 drain, export 또는 discard한다.
|
||||
2. worker, channel, lock과 runtime을 close한다.
|
||||
3. account-partition checkpoint/OPFS namespace를 maintenance-authorized cleanup한다.
|
||||
4. endpoint, worker registration, config, policy와 feature facade를 제거한다.
|
||||
5. production bundle/module inventory와 removal test로 source 부재를 증명한다.
|
||||
|
||||
rollback은 unbounded Blob fallback, validator 완화, digest 생략 또는 partial 자동
|
||||
append를 허용하지 않는다.
|
||||
|
||||
## 18. 완료 기준
|
||||
|
||||
Range resumable download는 다음이 모두 참일 때만 구현 완료다.
|
||||
|
||||
- `RANGE_RESUMABLE_DOWNLOAD_V1` port와 strict wire codec이 있음
|
||||
- capability mode별 exact allowed-status subset과 `200/206/412/416` 처리
|
||||
상태 머신이 실행 가능하게 검증됨
|
||||
- strong validator/immutable generation이 실제 provider에서 강제됨
|
||||
- checkpoint CAS, inventory, retention과 account cleanup이 구현됨
|
||||
- seek/truncate 또는 OPFS staging destination이 crash fault를 통과함
|
||||
- capability reissue가 representation mismatch를 fail-closed함
|
||||
- final whole-object SHA-256 뒤에만 verified success를 반환함
|
||||
- selector가 picker/seek 미지원과 대용량 fallback을 안전하게 결정함
|
||||
- actual BFF/provider와 Chromium/Firefox/WebKit evidence가 유효함
|
||||
- SLO, alert, runbook, kill switch, rollback과 cleanup drill이 승인됨
|
||||
|
||||
app-managed background download는 위 항목에 더해 다음이 필요하다.
|
||||
|
||||
- 지원 browser/deployment 범위가 명시됨
|
||||
- worker lifecycle 종료를 checkpoint로 복구함
|
||||
- worker control plane이 durable capability 저장 없이 동작함
|
||||
- logout/revocation/version upgrade fault가 통과함
|
||||
- 미지원 browser fallback이 동일 제품 요구를 안전하게 만족하거나 명시적
|
||||
unsupported UX를 가짐
|
||||
|
||||
이 기준 전에는 기존 foreground streaming 또는 browser handoff의 성공을 Range나
|
||||
background download 구현 완료 증거로 사용하지 않는다.
|
||||
|
||||
## 19. 선택하지 않은 대안
|
||||
|
||||
- `Range` header만 추가하고 기존 sequential writable에 append
|
||||
- weak ETag나 file name/lastModified를 representation identity로 사용
|
||||
- serialized incremental hash state를 검증 없이 checkpoint
|
||||
- `200` response를 기존 partial 뒤에 append
|
||||
- `416`을 곧바로 success로 해석
|
||||
- Service Worker keepalive를 cross-browser background 보장으로 간주
|
||||
- picker 미지원 대용량 파일을 unbounded Blob으로 fallback
|
||||
- OPFS staging을 사용자 파일 저장 완료로 표시
|
||||
- browser-managed handoff를 application-verified save로 표시
|
||||
- user-agent 문자열 기반 Safari 분기
|
||||
|
||||
## 20. 참고
|
||||
|
||||
- [Browser data capability completion ledger](../browser-data-capability-completion-ledger.md)
|
||||
- [VD-16 Browser transfer composition과 Image delivery](./VD-16-browser-transfer-composition-and-image-delivery.md)
|
||||
- [Browser transfer recovery](../../operations/browser-transfer-recovery.md)
|
||||
- [RFC 9110 HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110.html)
|
||||
- [Fetch Standard](https://fetch.spec.whatwg.org/)
|
||||
- [File System Standard](https://fs.spec.whatwg.org/)
|
||||
- [Service Workers](https://w3c.github.io/ServiceWorker/)
|
||||
- [Background Fetch draft](https://wicg.github.io/background-fetch/)
|
||||
- [기존 transfer 설계](../presigned-transfer-and-image-cdn.md)
|
||||
- [browser file/origin storage 설계](../browser-file-and-origin-storage.md)
|
||||
@@ -0,0 +1,858 @@
|
||||
# VD-15: Origin storage lifecycle, migration, and optional file capabilities
|
||||
|
||||
- 상태: Accepted design — implementation pending
|
||||
- 결정일: 2026-07-28
|
||||
- 현재 구현 상태: capability별로 아래 표에 명시
|
||||
- 이 ADR이 선택한 common delta의 목표 reference 상태:
|
||||
`AVAILABLE_NOT_COMPOSED`
|
||||
- 관련 결정: VD-10, VD-11, VD-12, VD-14
|
||||
- current status ledger:
|
||||
`docs/architecture/browser-data-capability-completion-ledger.md`
|
||||
- 적용 범위: File/Blob preview, file/directory selection, IndexedDB, OPFS,
|
||||
Cache Storage, StorageManager, optional Service Worker lifecycle
|
||||
|
||||
이 결정은 VD-11의 reference runtime을 실제 제품에 조립하기 전에 남아 있는
|
||||
origin-wide lifecycle과 migration 경계를 고정한다. 문서가 추가됐다는 사실은
|
||||
runtime 구현, bootstrap composition 또는 production traffic 승격을 의미하지
|
||||
않는다.
|
||||
|
||||
현재 구현돼 있는 것은 transient file vault, file picker, bounded download,
|
||||
IndexedDB repository/migration mechanism, OPFS object/journal runtime, public static
|
||||
Cache Storage release runtime과 StorageManager inspection primitive다. 다음은 아직
|
||||
구현되지 않았다.
|
||||
|
||||
- IndexedDB, OPFS, Cache Storage를 함께 조정하는 pressure/write-admission/GC
|
||||
coordinator
|
||||
- origin 전체 eviction을 완전하게 판별하는 mechanism
|
||||
- OPFS physical layout/journal과 Cache control schema의 forward migration runtime
|
||||
- 실제 OPFS write/read/delete readiness probe
|
||||
- cursor/deadline이 있는 bounded Cache Storage inspection/cleanup
|
||||
- Service Worker update/client-drain controller
|
||||
- local preview의 pixel/decode/frame safety probe
|
||||
- directory/persistent handle/drag-and-drop capability
|
||||
- Range/206 download 또는 private/range response cache
|
||||
|
||||
## 1. 상태 모델과 현재/목표
|
||||
|
||||
### 1.1 다섯 primary current-status literal
|
||||
|
||||
capability의 source 구현, 제품 선택, composition, traffic과 evidence를 하나의
|
||||
`enabled` boolean으로 합치지 않는다. 이 결정에서 capability의 **primary current
|
||||
status**로 허용하는 literal은 정확히 다음 다섯 가지다.
|
||||
|
||||
| primary status | 의미 |
|
||||
| --- | --- |
|
||||
| `COMPOSED` | 실제 owner/policy/provider가 production composition root에 연결돼 있다. traffic이 disabled/canary/enabled인지는 이 상태가 아니라 별도 admission 축이다. |
|
||||
| `AVAILABLE_NOT_COMPOSED` | 실행 가능한 runtime과 test가 source에 있지만 production bootstrap과 제품 dataset에는 연결하지 않았다. |
|
||||
| `DESIGNED_NOT_IMPLEMENTED` | port, invariant, failure/recovery와 promotion 기준은 결정됐지만 실행 가능한 runtime이 없다. |
|
||||
| `NOT_SELECTED` | 제품 요구와 owner가 capability를 선택하지 않았다. source 설계나 일부 primitive가 있더라도 runtime, DB, worker, listener를 만들지 않는다. |
|
||||
| `PLATFORM_LIMITED` | 요구한 의미를 대상 browser 전체에서 application-controlled capability로 보장할 수 없다. 지원 engine의 progressive enhancement와 명시적 fallback만 허용한다. |
|
||||
|
||||
이 다섯 값은 선형 maturity 단계가 아니다. 예를 들어 구현이 존재해도 제품이
|
||||
선택하지 않은 별도 capability의 primary status는 `NOT_SELECTED`일 수 있고,
|
||||
cross-browser 보장이 불가능하면 구현량과 무관하게 `PLATFORM_LIMITED`다.
|
||||
`COMPOSED`도 traffic enablement나 runtime health를 암묵적으로 뜻하지 않는다.
|
||||
|
||||
primary status와 별도로 다음 축을 기록한다.
|
||||
|
||||
```text
|
||||
Selection
|
||||
NOT_SELECTED | SELECTED | REMOVING
|
||||
|
||||
TrafficAdmission
|
||||
DISABLED | SHADOW | CANARY | ENABLED
|
||||
|
||||
RuntimeHealth
|
||||
UNKNOWN | AVAILABLE | DEGRADED | UNAVAILABLE | INCOMPATIBLE
|
||||
|
||||
PromotionEvidence
|
||||
MISSING | PARTIAL | COMPLETE | EXPIRED
|
||||
```
|
||||
|
||||
이 네 이름과 literal은 completion ledger의 canonical readiness 축이다.
|
||||
`PromotionEvidence`는 별도 임의 enum이 아니라 ledger의 contract, provider,
|
||||
browser, operations component gate를 합성한 값이다. required artifact가 없으면
|
||||
`MISSING`, 일부만 terminal이면 `PARTIAL`, 모든 required gate와 freshness가
|
||||
충족될 때만 `COMPLETE`, 한 번 유효했던 required artifact가 만료되면
|
||||
`EXPIRED`로 기록한다.
|
||||
|
||||
예를 들어 실제 dependency를 조립한 첫 배포는
|
||||
`primaryStatus=COMPOSED`, `TrafficAdmission=DISABLED`,
|
||||
`RuntimeHealth=UNKNOWN`, `PromotionEvidence=PARTIAL`일 수 있다. probe와 canary
|
||||
승격은 primary status를 새 literal로 바꾸지 않고 별도 축만 변경한다.
|
||||
|
||||
rollback은 `TrafficAdmission`을 먼저 `DISABLED`로 내린다. schema version을
|
||||
내리거나, user-authored data를 자동 삭제하거나, unavailable runtime을 in-memory
|
||||
fake로 교체하지 않는다.
|
||||
|
||||
### 1.2 current vs target
|
||||
|
||||
| capability | 현재 | 이 결정의 목표 | 비고 |
|
||||
| --- | --- | --- | --- |
|
||||
| transient File/Blob vault와 picker | `AVAILABLE_NOT_COMPOSED` | 유지 | 제품 policy가 없으므로 조립하지 않음 |
|
||||
| foreground streaming/save와 browser handoff | `AVAILABLE_NOT_COMPOSED` | 유지 | Range resume는 포함하지 않음 |
|
||||
| IndexedDB repository/codec migration | `AVAILABLE_NOT_COMPOSED` | 유지, coordinator hook 추가 대상 | 제품 dataset/schema는 없음 |
|
||||
| OPFS object/journal v1 | `AVAILABLE_NOT_COMPOSED` | 유지 | 현재 byte runtime과 v1 reconciliation 범위 |
|
||||
| OPFS real readiness preflight | `DESIGNED_NOT_IMPLEMENTED` | `AVAILABLE_NOT_COMPOSED` | property probe/native test를 composition readiness로 오인하지 않음 |
|
||||
| public static Cache release v1 | `AVAILABLE_NOT_COMPOSED` | 유지 | 현재 stage/activate/previous retain은 구현 |
|
||||
| bounded Cache inspect/cleanup | `DESIGNED_NOT_IMPLEMENTED` | `AVAILABLE_NOT_COMPOSED` | 현재 ownership은 검증하지만 cache-count scan은 unbounded |
|
||||
| StorageManager estimate/persist primitive | `AVAILABLE_NOT_COMPOSED` | 유지 | origin coordinator는 없음 |
|
||||
| origin storage lifecycle coordinator | `DESIGNED_NOT_IMPLEMENTED` | `AVAILABLE_NOT_COMPOSED` | 이 ADR이 계약을 확정 |
|
||||
| OPFS physical/journal forward migration | `DESIGNED_NOT_IMPLEMENTED` | `AVAILABLE_NOT_COMPOSED` | 기존 v1 layout/journal을 migrator 구현으로 오인하지 않음 |
|
||||
| Cache control/prefix forward migration | `DESIGNED_NOT_IMPLEMENTED` | `AVAILABLE_NOT_COMPOSED` | 기존 v1 parser/release primitive를 migrator 구현으로 오인하지 않음 |
|
||||
| local preview safety probe | `DESIGNED_NOT_IMPLEMENTED` | `AVAILABLE_NOT_COMPOSED` | 현재 byte/signature check만 있음 |
|
||||
| Service Worker update lifecycle | `NOT_SELECTED` | 제품이 PWA를 선택할 때 별도 승격 | Cache Storage 사용만으로 자동 선택하지 않음 |
|
||||
| directory selection/persistent handles/drop | `NOT_SELECTED` | 제품 workspace 요구가 있을 때 별도 승격 | consent/retention 결정 필요 |
|
||||
| Range resumable download | `DESIGNED_NOT_IMPLEMENTED` | VD-14의 별도 capability | backend validator/range 계약과 별도 runtime 필요 |
|
||||
| sparse Range response cache | `NOT_SELECTED` | Range download와도 분리된 별도 capability | segment merge/cache threat model 필요 |
|
||||
| private response cache | `NOT_SELECTED` | 별도 security review 전 금지 | public cache를 확장해 암묵 설치하지 않음 |
|
||||
| app-managed background download | `NOT_SELECTED` | 지원 browser용 별도 optional capability | 별도 owner/staging/worker protocol 필요 |
|
||||
| cross-browser app-managed background download guarantee | `PLATFORM_LIMITED` | browser-managed handoff를 기본 fallback으로 유지 | Service Worker가 장시간 transfer 지속을 보장하지 않음 |
|
||||
|
||||
## 2. 변경할 수 없는 불변조건
|
||||
|
||||
1. `navigator.storage.estimate()`는 rough origin signal이지 free-space reservation,
|
||||
per-store usage 또는 eviction guarantee가 아니다.
|
||||
2. 실제 `QuotaExceededError`가 write failure의 authority다. estimate가 정상이어도
|
||||
write는 실패할 수 있다.
|
||||
3. IndexedDB, OPFS와 Cache Storage 사이에는 atomic transaction이 없다. coordinator는
|
||||
saga와 idempotency를 제공할 뿐 cross-API ACID를 주장하지 않는다.
|
||||
4. user-authored/unsynced data는 pressure 또는 migration convenience를 이유로 자동
|
||||
삭제하지 않는다.
|
||||
5. credential, session token, raw authorization header, presigned URL과 signing key는
|
||||
어느 origin store에도 저장하지 않는다.
|
||||
6. migration은 expand/migrate/contract 순서다. committed OPFS file을 in-place로
|
||||
변환하지 않고, Cache Storage의 검증되지 않은 candidate를 active로 만들지 않는다.
|
||||
7. future schema를 이전 bundle이 발견하면 destructive open/delete 대신 read-only,
|
||||
online-only 또는 export-required로 전환한다.
|
||||
8. capability probe failure를 fake success로 바꾸지 않는다.
|
||||
9. Service Worker 등록·활성화와 Cache Storage ownership은 서로 다른 capability다.
|
||||
10. local image preview는 encoded byte cap만으로 decode safety를 주장하지 않는다.
|
||||
11. directory handle과 persistent file handle은 transient file selection의 자연스러운
|
||||
연장이 아니라 별도 consent/persistence capability다.
|
||||
12. public Cache Storage는 private/auth/range representation의 repository가 아니다.
|
||||
|
||||
## 3. composition과 owner/policy injection
|
||||
|
||||
### 3.1 composition root
|
||||
|
||||
제품이 선택하면 한 composition root가 다음 dependency를 immutable snapshot으로
|
||||
고정한다.
|
||||
|
||||
```text
|
||||
OriginStorageLifecycleComposition
|
||||
originScope
|
||||
releaseId
|
||||
datasetPolicyRegistry
|
||||
storageDurability
|
||||
indexedDbMaintenance[]
|
||||
opfsMaintenance[]
|
||||
publicCacheMaintenance[]
|
||||
mutationLock
|
||||
clock
|
||||
scheduler
|
||||
lifecycleAuthority
|
||||
safeObserver
|
||||
killSwitches
|
||||
```
|
||||
|
||||
page, hook 또는 domain use case는 native manager와 maintenance adapter를 직접
|
||||
조합하지 않는다. coordinator에 등록되는 각 dataset profile은 다음을 필수로
|
||||
소유한다.
|
||||
|
||||
| 필드 | owner가 결정할 내용 |
|
||||
| --- | --- |
|
||||
| `datasetRegistryId` | readable user/account 값이 아닌 고정된 registry ID |
|
||||
| `owner` | product owner와 operational owner |
|
||||
| `technology` | IndexedDB, OPFS, public Cache 중 정확한 storage |
|
||||
| `authority` | server, local-first, reconstructable |
|
||||
| `classification` | public, internal, personal, confidential |
|
||||
| `accountScope` | origin-shared 또는 opaque partition |
|
||||
| `retention` | session, TTL, until-synced, explicit delete |
|
||||
| `soft/hardBudget` | logical dataset budget; native free-space claim이 아님 |
|
||||
| `pressurePriority` | expired/reconstructable, synced-copy, user-authored 순서 |
|
||||
| `writeCriticality` | essential user write, sync receipt, reconstructable cache |
|
||||
| `fallback` | read-only, online-only, export-required |
|
||||
| `migrationOwner` | schema/codec/layout migration과 rollback owner |
|
||||
| `recoveryOwner` | rehydrate, export, backend sync와 incident owner |
|
||||
|
||||
registry는 composition 시 deep snapshot/freeze하고 같은 문자열을 가진 caller-created
|
||||
profile을 identity로 인정하지 않는다. presentation은 priority, retention 또는
|
||||
eviction eligibility를 요청별로 고를 수 없다.
|
||||
|
||||
### 3.2 authority가 필요한 동작
|
||||
|
||||
logout, account deletion, until-synced deletion과 user-authored export/purge는 제품
|
||||
authority를 요구한다. 기존 OPFS maintenance proof와 동일하게 provider가 exact
|
||||
reason/scope/policy에 묶인 짧은 proof를 발급하고 consumer가 원자적으로
|
||||
consume한다.
|
||||
|
||||
pressure에 따른 expired/reconstructable GC는 product proof가 없어도 실행할 수
|
||||
있지만 등록 policy와 bounded budget을 벗어나면 안 된다. `SYNCED_COPY` 삭제는
|
||||
authoritative server revision 또는 별도 sync receipt가 확인된 항목에만 허용한다.
|
||||
|
||||
## 4. origin-wide pressure, write admission, and GC
|
||||
|
||||
### 4.1 coordinator port
|
||||
|
||||
목표 runtime은 native type을 노출하지 않는 다음 의미의 port를 제공한다.
|
||||
|
||||
```ts
|
||||
type PressureState =
|
||||
| "UNKNOWN"
|
||||
| "NORMAL"
|
||||
| "PRESSURE"
|
||||
| "CRITICAL"
|
||||
| "QUOTA_FAILURE";
|
||||
|
||||
type WriteAdmission =
|
||||
| { kind: "ADMITTED"; admissionId: string; attempt: 1 | 2 }
|
||||
| { kind: "DEFERRED"; recovery: "RETRY" | "ONLINE_ONLY" }
|
||||
| { kind: "DENIED"; recovery: "READ_ONLY" | "EXPORT_REQUIRED" };
|
||||
```
|
||||
|
||||
실제 API는 repository write를 대신하지 않는다. 각 adapter가 write 직전 admission을
|
||||
얻고, commit/rollback 이후 exact admission을 완료하도록 좁은 hook을 받는다.
|
||||
admission ID는 diagnostic 또는 persistence에 남기지 않는 runtime-local fencing
|
||||
token이다.
|
||||
|
||||
### 4.2 pressure state와 hysteresis
|
||||
|
||||
기본 threshold 시작점은 VD-11과 일치한다.
|
||||
|
||||
| 진입 조건 | 상태 |
|
||||
| --- | --- |
|
||||
| usage/quota를 모름 | `UNKNOWN` |
|
||||
| `< 70%` | `NORMAL` |
|
||||
| `>= 70%` | `PRESSURE` |
|
||||
| `>= 85%` | `CRITICAL` |
|
||||
| 실제 write의 quota exception | `QUOTA_FAILURE` |
|
||||
|
||||
flapping을 막기 위해 하향 전이는 더 낮은 threshold를 사용한다.
|
||||
|
||||
- `CRITICAL -> PRESSURE`: 두 번 연속 inspection에서 `< 80%`
|
||||
- `PRESSURE -> NORMAL`: 두 번 연속 inspection에서 `< 65%`
|
||||
- inspection 간격은 composition policy가 정하되 boot polling loop를 만들지 않는다.
|
||||
- tab마다 독립 GC하지 않는다. 고정된 origin Web Lock 아래 leader 하나만 maintenance를
|
||||
수행하고, lock이 없으면 destructive maintenance를 하지 않는다.
|
||||
|
||||
두 번 연속 규칙은 in-memory observation일 뿐 영구 storage truth가 아니다. 새
|
||||
runtime은 persisted pressure state를 맹신하지 않고 `UNKNOWN`에서 시작한다.
|
||||
|
||||
### 4.3 admission matrix
|
||||
|
||||
| pressure | essential user-authored | sync/export receipt | reconstructable/cache |
|
||||
| --- | --- | --- | --- |
|
||||
| `UNKNOWN` | policy hard budget 안에서 허용, failure 대비 | 허용 | 보수적으로 defer 가능 |
|
||||
| `NORMAL` | 허용 | 허용 | 허용 |
|
||||
| `PRESSURE` | 허용 | 허용 | 먼저 bounded GC, 신규 speculative write 제한 |
|
||||
| `CRITICAL` | hard budget과 recovery path가 있을 때만 허용 | 허용 우선 | 거절/online-only |
|
||||
| `QUOTA_FAILURE` | rollback 후 export/sync UX | rollback 후 retry 조건 평가 | rollback, GC, 최대 1회 retry |
|
||||
|
||||
estimate만으로 “N bytes를 예약했다”고 기록하지 않는다. IndexedDB/OPFS의 logical
|
||||
budget reservation은 동시 writer 간 policy ceiling을 강제하기 위한 값이며 origin
|
||||
free space가 아니다.
|
||||
|
||||
### 4.4 GC 순서와 bounded execution
|
||||
|
||||
GC 순서는 모든 기술에서 다음 우선순위를 유지한다.
|
||||
|
||||
```text
|
||||
incomplete candidate / stale staging
|
||||
-> expired reconstructable
|
||||
-> unreferenced immutable chunk with grace
|
||||
-> inactive public cache release
|
||||
-> confirmed synced copy
|
||||
-> stop
|
||||
```
|
||||
|
||||
user-authored/unsynced는 자동 GC 목록에 들어가지 않는다. 각 invocation은 다음
|
||||
두 예산을 모두 가진다.
|
||||
|
||||
- 기본 최대 100 items 또는 5초
|
||||
- 구현 절대 상한 500 items 또는 30초
|
||||
|
||||
각 native operation 사이에 deadline과 AbortSignal을 다시 확인한다. 결과는
|
||||
`inspected`, `removed`, `releasedLogicalBytes`, `moreAvailable`,
|
||||
`deadlineReached`와 opaque cursor를 반환한다. cursor는 dataset/policy/release
|
||||
epoch에 묶고 다른 owner에서 replay하면 `STALE_RESULT`다.
|
||||
|
||||
### 4.5 quota failure 뒤 단 한 번의 retry
|
||||
|
||||
자동 retry는 다음 조건을 전부 만족할 때만 허용한다.
|
||||
|
||||
1. 첫 attempt가 실제 `QuotaExceededError`로 rollback됐다.
|
||||
2. operation이 같은 idempotency key, revision fence와 payload digest를 가진다.
|
||||
3. 외부 side effect 또는 cross-store publish가 commit되지 않았다.
|
||||
4. bounded GC가 실제로 candidate를 제거했거나 pressure가 하향됐다.
|
||||
5. retry가 같은 operation lifecycle에서 정확히 한 번뿐이다.
|
||||
6. 새 admission token을 발급하고 현재 revision/generation을 다시 읽는다.
|
||||
|
||||
두 번째 quota failure, partial external commit, user-authored destructive overwrite,
|
||||
unknown idempotency는 retry하지 않는다. recovery는 policy에 따라 `READ_ONLY`,
|
||||
`ONLINE_ONLY` 또는 `EXPORT_REQUIRED`다.
|
||||
|
||||
## 5. eviction detection의 범위와 한계
|
||||
|
||||
### 5.1 감지할 수 있는 것
|
||||
|
||||
각 조립된 dataset은 opaque scope에 다음 binding을 둔다.
|
||||
|
||||
- IndexedDB governance row와 dataset epoch
|
||||
- OPFS journal logical object와 physical manifest/digest
|
||||
- public Cache active pointer와 verified release marker
|
||||
- 선택적으로 backend가 알고 있는 opaque dataset installation epoch
|
||||
|
||||
다음 partial mismatch는 `STORAGE_EVICTED` 또는 `CORRUPT_DATA`로 구분할 수 있다.
|
||||
|
||||
- logical OPFS object는 있는데 physical manifest/chunk가 없음
|
||||
- Cache active pointer는 있는데 candidate cache/marker가 없음
|
||||
- migration checkpoint는 있는데 target generation이 없음
|
||||
- expected dataset epoch와 local governance binding이 다름
|
||||
|
||||
reconstructable data는 rehydrate하고, local-first/user-authored data는 자동 empty
|
||||
state로 초기화하지 않고 read-only/export-required incident로 올린다.
|
||||
|
||||
### 5.2 감지할 수 없는 것
|
||||
|
||||
browser가 origin의 IndexedDB, OPFS와 Cache Storage를 모두 함께 지우면 local
|
||||
sentinel도 함께 사라진다. local state만으로 다음 두 상황을 완전하게 구분할 수
|
||||
없다.
|
||||
|
||||
```text
|
||||
이 browser의 첫 설치
|
||||
origin storage 전체 eviction/user clear
|
||||
```
|
||||
|
||||
따라서 “sentinel이 없으므로 첫 설치”라고 단정하지 않는다. 제품이 구분을 요구하면
|
||||
현재 인증 session의 backend에 opaque installation/dataset epoch를 보관하고
|
||||
authorization 후 비교해야 한다. backend marker도 browser byte backup이 아니며,
|
||||
local-only data 복구를 보장하지 않는다.
|
||||
|
||||
backend epoch가 없으면 UI는 empty/new와 storage-reset-possible 상태를 제품 정책에
|
||||
맞게 합쳐 표현해야 한다. raw account ID, filename, object ID 또는 digest를
|
||||
sentinel/log에 넣지 않는다.
|
||||
|
||||
## 6. schema, codec, physical migration
|
||||
|
||||
### 6.1 독립 version 축
|
||||
|
||||
다음 version을 하나의 숫자로 합치지 않는다.
|
||||
|
||||
| 축 | 의미 | 현재 |
|
||||
| --- | --- | --- |
|
||||
| IndexedDB DDL | store/index/governance shape | reference runtime에 additive planner 있음 |
|
||||
| IndexedDB record codec | payload decode/encode | resumable maintenance mechanism 있음 |
|
||||
| OPFS journal DDL | logical object/journal/budget/refcount schema | v1 고정 |
|
||||
| OPFS physical layout | root/path/manifest/chunk-tree algorithm | v1 고정 |
|
||||
| Cache control schema | marker/active pointer JSON | v1 고정 |
|
||||
| Cache release manifest | URL/header/type/length/digest binding | current static release contract |
|
||||
| lifecycle registry schema | owner/policy/admission binding | 이 결정에서 설계, 구현 없음 |
|
||||
|
||||
IndexedDB mechanism이 존재한다고 해서 제품 codec/migration이 자동으로 존재하는
|
||||
것은 아니다. OPFS와 Cache v1 parser가 있다는 사실도 forward migration 구현을
|
||||
뜻하지 않는다.
|
||||
|
||||
### 6.2 공통 expand/migrate/contract
|
||||
|
||||
1. **expand:** 새 reader가 N과 N-1을 읽고 새 metadata/checkpoint를 additive하게
|
||||
추가한다.
|
||||
2. **drain:** old writer가 더는 N-1 shape를 쓰지 않는다는 release/lease evidence를
|
||||
확인한다.
|
||||
3. **migrate:** bounded batch와 keyset/opaque cursor로 copy/verify한다.
|
||||
4. **publish:** row, checkpoint, logical budget과 generation fence를 가능한 한 같은
|
||||
native transaction에서 commit한다.
|
||||
5. **observe:** canary와 rollback window 동안 N-1 reader compatibility를 확인한다.
|
||||
6. **contract:** 모든 active/rollback release가 지난 별도 release에서만 old shape를
|
||||
정리한다.
|
||||
|
||||
schema downgrade, blanket database/cache/root deletion과 read-time unbounded rewrite는
|
||||
금지한다.
|
||||
|
||||
### 6.3 OPFS migration
|
||||
|
||||
OPFS physical migration은 copy-on-write다.
|
||||
|
||||
```text
|
||||
v1 committed object
|
||||
-> v2 staging transaction
|
||||
-> bounded chunk copy/read
|
||||
-> v2 manifest + tree digest verify
|
||||
-> IDB journal generation/fencing CAS
|
||||
-> v2 logical publish
|
||||
-> rollback window 동안 v1 retain
|
||||
-> authority 확인 후 v1 bounded cleanup
|
||||
```
|
||||
|
||||
- committed v1 file/chunk를 in-place로 수정하지 않는다.
|
||||
- checkpoint는 last logical object key와 source/target generation을 저장한다.
|
||||
- source digest, target digest, bytes와 policy binding이 맞지 않으면 quarantine하고
|
||||
다음 object로 성공 처리하지 않는다.
|
||||
- crash가 v2 publish 전이면 v1이 authority다.
|
||||
- publish 후 cleanup crash는 v2가 authority이고 cleanup을 재개한다.
|
||||
- N-1 bundle은 v2를 쓰지 않고 read-only/online-only로 degrade한다.
|
||||
- local-first bytes를 contract하려면 export/sync 또는 승인된 rollback-window
|
||||
evidence가 필요하다.
|
||||
|
||||
### 6.4 Cache migration과 rollback
|
||||
|
||||
public Cache data는 reconstructable이므로 byte-by-byte schema rewrite보다 새
|
||||
release를 다시 stage/verify/activate한다. Service Worker를 선택하지 않은 static
|
||||
Cache-only 조합의 migration은 다음 흐름이다.
|
||||
|
||||
```text
|
||||
old active verified release
|
||||
-> new prefix/control schema candidate
|
||||
-> exact network fetch + integrity verify
|
||||
-> explicit activation
|
||||
-> old + previous retain
|
||||
-> composition-owned rollback/grace window 확인
|
||||
-> bounded owned-prefix cleanup
|
||||
```
|
||||
|
||||
이 흐름에는 waiting worker, `controllerchange` 또는 controlled-client drain을
|
||||
성공 조건으로 넣지 않는다. Service Worker를 별도 선택한 조합만 section 9.2의
|
||||
waiting/activation protocol을 실행하고, old controlled client가 drain된 뒤 해당
|
||||
release를 cleanup eligible로 만든다.
|
||||
|
||||
rollback은 검증된 previous release의 ID와 manifest digest로 같은 activation
|
||||
protocol을 다시 실행한다. caller가 raw cache name이나 retain list를 전달하지
|
||||
않는다. new control schema가 unreadable하면 old pointer를 덮어쓰지 않고
|
||||
network-only로 degrade한다.
|
||||
|
||||
Cache control/release cleanup도 section 4의 cursor/deadline 예산을 적용한다.
|
||||
unregister 또는 새 Service Worker install만으로 cache migration이 완료됐다고
|
||||
보지 않는다.
|
||||
|
||||
### 6.5 N-1 rollback contract
|
||||
|
||||
모든 durable migration은 최소 다음 fixture를 보유한다.
|
||||
|
||||
- N-1 fresh -> N open
|
||||
- N-1 populated -> N partial migration crash -> N resume
|
||||
- N migration 완료 -> N-1 open: destructive write 없이 read-only/online-only
|
||||
- N canary rollback -> N-1 server path로 정상 동작
|
||||
- N rollback window 종료 뒤 별도 contract release
|
||||
|
||||
rollback bundle은 schema number를 낮추지 않는다. 새로운 writer를 끄고 compatible
|
||||
reader/fallback을 사용한다.
|
||||
|
||||
## 7. OPFS real readiness preflight
|
||||
|
||||
현재 `inspectBrowserOpfsSupport()`는 API property를 확인한다. 목표 preflight는
|
||||
실제 작은 operation을 검증한다.
|
||||
|
||||
### 7.1 probe protocol
|
||||
|
||||
probe는 `primaryStatus=COMPOSED`, `Selection=SELECTED`이고 readiness 확인이
|
||||
필요할 때 실행한다. 최초 composition에서는 `TrafficAdmission=DISABLED` 또는
|
||||
`SHADOW`로 probe하며, 선택하지 않은 skeleton boot에서 OPFS root/DB/worker를
|
||||
만들지 않는다.
|
||||
|
||||
```text
|
||||
secure context/API check
|
||||
-> DedicatedWorker boot + protocol handshake
|
||||
-> origin Web Lock acquire
|
||||
-> owned opaque probe scope의 IDB journal transaction
|
||||
-> random staging file create
|
||||
-> bounded bytes write + flush/close
|
||||
-> read + length/digest verify
|
||||
-> file/journal cleanup
|
||||
-> lock/worker/connection close
|
||||
```
|
||||
|
||||
규칙:
|
||||
|
||||
- main thread에서 SyncAccessHandle을 만들지 않는다.
|
||||
- synchronous path와 configured async writable fallback을 각각 capability로
|
||||
보고한다.
|
||||
- probe object ID/path는 secure random opaque value이고 log에 기록하지 않는다.
|
||||
- 기본 deadline 5초, 절대 상한 30초다.
|
||||
- timeout/crash 뒤 stale probe는 reconciliation owner가 grace 후 bounded cleanup한다.
|
||||
- 결과는 runtime memory에 짧게 cache할 수 있지만 browser update, visibility가 긴
|
||||
sleep에서 복귀, quota/permission failure 뒤 다시 `UNKNOWN`으로 돌린다.
|
||||
- probe 성공은 future write 또는 persistence guarantee가 아니다.
|
||||
|
||||
### 7.2 readiness mapping
|
||||
|
||||
| 결과 | runtime health | admission |
|
||||
| --- | --- | --- |
|
||||
| full worker/lock/journal/write/read/delete 성공 | `AVAILABLE` | policy에 따라 가능 |
|
||||
| sync handle 없음, 승인된 async fallback 성공 | `DEGRADED` | size/concurrency ceiling 하향 |
|
||||
| API 없음/secure context 아님 | `UNAVAILABLE` | online-only |
|
||||
| protocol/schema mismatch | `INCOMPATIBLE` | read/write 금지 |
|
||||
| timeout/quota/permission | `DEGRADED` 또는 `UNAVAILABLE` | 신규 write 금지, recovery 실행 |
|
||||
|
||||
## 8. bounded Cache Storage maintenance
|
||||
|
||||
현재 static public cache는 release stage/verify/activate, previous retain과
|
||||
owned-prefix cleanup을 구현한다. 현재 `cleanupOwned()`와 `inspect()`에는
|
||||
max-count/deadline/cursor가 없다. 목표 contract는 이를 bounded operation으로
|
||||
바꾼다.
|
||||
|
||||
```ts
|
||||
type CacheMaintenancePage = Readonly<{
|
||||
inspectedCaches: number;
|
||||
deletedCaches: number;
|
||||
retainedCaches: number;
|
||||
unreadableCaches: number;
|
||||
nextCursor: string | null;
|
||||
moreAvailable: boolean;
|
||||
deadlineReached: boolean;
|
||||
}>;
|
||||
```
|
||||
|
||||
- default 100 caches/5초, absolute 500 caches/30초
|
||||
- cursor는 owned prefix, active pointer epoch와 policy fingerprint에 binding
|
||||
- caller는 raw cache name, retain list 또는 prefix를 제출할 수 없음
|
||||
- mutation Web Lock 아래 active pointer를 다시 읽은 뒤 한 cache씩 처리
|
||||
- abort/deadline 뒤 이미 완료한 delete truth는 되돌리지 않고 cursor부터 재개
|
||||
- corrupt active pointer면 destructive cleanup을 중지하고 network-only
|
||||
- unreadable inactive candidate는 grace와 current/previous binding 확인 뒤 삭제
|
||||
- `QuotaExceededError`를 이유로 다른 origin cache나 user data를 삭제하지 않음
|
||||
|
||||
inspection도 동일한 page contract를 써서 cache 수에 비례한 unbounded boot work를
|
||||
금지한다.
|
||||
|
||||
## 9. static Cache release와 optional Service Worker lifecycle
|
||||
|
||||
### 9.1 현재 static release capability
|
||||
|
||||
현재 adapter가 소유하는 범위:
|
||||
|
||||
- same-origin anonymous public GET
|
||||
- exact query/request header/Vary
|
||||
- type, declared/actual length와 SHA-256
|
||||
- candidate 전체 성공 뒤 explicit activation
|
||||
- failed candidate 삭제와 기존 active 유지
|
||||
- active + verified previous release retain
|
||||
- private/no-store/auth/opaque/redirect/206 거부
|
||||
|
||||
Window 또는 Worker에서 Cache Storage를 쓸 수 있으므로 이 기능은 Service Worker
|
||||
설치를 의미하지 않는다.
|
||||
|
||||
### 9.2 Service Worker를 선택할 때의 별도 protocol
|
||||
|
||||
PWA/offline interception을 제품이 선택하면 별도 owner가 다음 lifecycle을
|
||||
composition한다.
|
||||
|
||||
```text
|
||||
installing worker
|
||||
-> candidate static release stage/verify
|
||||
-> waiting
|
||||
-> page update controller:
|
||||
dirty form / active transfer / compatibility 확인
|
||||
-> explicit ACTIVATE(version, manifest)
|
||||
-> pointer flip
|
||||
-> skipWaiting opt-in
|
||||
-> controllerchange acknowledgement
|
||||
-> old clients drain
|
||||
-> clients.claim opt-in
|
||||
-> previous release grace retain
|
||||
-> bounded cleanup
|
||||
```
|
||||
|
||||
`skipWaiting()`과 `clients.claim()`을 install handler에서 자동 호출하지 않는다.
|
||||
message는 protocol version, release ID, nonce와 exact target worker에 binding하고
|
||||
unknown message를 drop한다.
|
||||
|
||||
fetch 전략은 route registry에 고정한다.
|
||||
|
||||
| route class | 허용 전략 |
|
||||
| --- | --- |
|
||||
| content-hashed static asset | exact active cache-first |
|
||||
| navigation | network-first + 별도 검증된 static offline page |
|
||||
| runtime config/release manifest/auth/API | network-only |
|
||||
| approved public runtime media | 별도 TTL metadata owner가 있을 때만 bounded SWR |
|
||||
|
||||
runtime TTL/SWR은 static release adapter의 묵시적 기능이 아니다. 별도 entry/count/
|
||||
byte/TTL budget, revalidation owner와 prune cursor가 있어야 한다.
|
||||
|
||||
Service Worker는 application-controlled long-running background download를
|
||||
cross-browser로 보장하지 않는다. download lifecycle은 VD-14의 별도 capability다.
|
||||
|
||||
## 10. local preview decode safety
|
||||
|
||||
### 10.1 현재와 목표
|
||||
|
||||
현재 preview path는 selection byte cap, signature receipt, media allowlist,
|
||||
active-content denylist와 object URL lease를 제공한다. static raster의 intrinsic
|
||||
dimensions, decoded surface와 animation frame 수를 검사하지 않는다.
|
||||
|
||||
목표 runtime은 object URL을 발급하기 전에 exact registered preview policy에
|
||||
묶인 `PreviewSafetyProbePort`를 호출한다.
|
||||
|
||||
### 10.2 policy와 검사 순서
|
||||
|
||||
owner가 최소 다음을 결정한다.
|
||||
|
||||
- 허용 static format과 signature parser version
|
||||
- max encoded bytes
|
||||
- max width/height
|
||||
- max total pixels
|
||||
- max decoded bytes
|
||||
- animation 허용 여부와 max frames/total pixels
|
||||
- decode concurrency와 deadline
|
||||
- malformed/unsupported metadata 동작
|
||||
|
||||
기본은 JPEG, PNG, WebP, AVIF 중 검토된 static parser만 허용하고 animation,
|
||||
SVG, HTML, XML, PDF는 preview에서 거절한다. animation이 제품 요구면 별도
|
||||
frame/time/memory capability로 승격한다.
|
||||
|
||||
```text
|
||||
bounded header read
|
||||
-> container/signature parse
|
||||
-> width/height/frame/static 여부
|
||||
-> overflow-safe pixel/decoded-byte 계산
|
||||
-> optional real bitmap decode
|
||||
-> decoded dimensions exact match
|
||||
-> bitmap close
|
||||
-> object URL lease 발급
|
||||
```
|
||||
|
||||
`width * height * 4` 계산은 safe integer overflow를 검사한다. parser header만
|
||||
신뢰하지 않고 지원 browser에서는 `createImageBitmap` 등 실제 decode를 bounded
|
||||
concurrency/deadline 아래 확인하고 즉시 `close()`한다. decode failure 뒤 object
|
||||
URL을 발급하지 않는다.
|
||||
|
||||
runtime absolute ceiling은 product policy보다 크거나 같고 caller는 낮출 수만 있다.
|
||||
원본 filename, digest와 dimensions를 telemetry에 기록하지 않고 bucket만 남긴다.
|
||||
|
||||
## 11. directory, persistent handles, and drag-and-drop
|
||||
|
||||
세 기능은 현재 transient picker port에 추가하지 않는다.
|
||||
|
||||
### 11.1 directory selection
|
||||
|
||||
제품이 folder import/workspace를 선택하면 별도 `DirectorySelectionPort`를 만든다.
|
||||
|
||||
- `showDirectoryPicker`는 progressive enhancement
|
||||
- `<input webkitdirectory>`는 검증된 baseline으로만 사용
|
||||
- depth, entry count, per-file/total bytes, traversal time의 hard cap
|
||||
- relative path segment NFC 정규화, `.`/`..`, separator, control/bidi 거부
|
||||
- 파일이 아닌 entry, traversal 중 permission loss와 mutation을 closed failure로
|
||||
처리
|
||||
- traversal 결과는 opaque file refs와 sanitized relative metadata만 반환
|
||||
- directory name/path를 domain ID 또는 log로 사용하지 않음
|
||||
|
||||
directory upload가 필요하면 backend도 archive/path/symlink/traversal과 total
|
||||
expanded budget을 다시 검증한다.
|
||||
|
||||
### 11.2 persistent handles and permission
|
||||
|
||||
persistent handle은 별도 registry와 consent가 필요하다.
|
||||
|
||||
- IDB structured-clone support를 실제 probe
|
||||
- handle 자체를 application/domain/query cache에 노출하지 않음
|
||||
- opaque handle ref, account partition, purpose, retention과 last-used bucket만 보관
|
||||
- boot/background에서 `requestPermission()` 금지
|
||||
- explicit user action에서 `queryPermission()` 후 필요한 경우에만 request
|
||||
- denied/revoked/stale handle은 `RESELECT`, silent empty file로 처리하지 않음
|
||||
- logout/account deletion과 handle registry purge는 authority를 요구
|
||||
- browser가 OS 권한 철회를 지원하지 않을 수 있음을 UX에 명시
|
||||
|
||||
handle persistence는 local bytes backup이 아니며 파일이 외부에서 바뀔 수 있다.
|
||||
매 open마다 size/lastModified와 제품이 요구하는 content identity를 다시 검사한다.
|
||||
|
||||
### 11.3 drag-and-drop
|
||||
|
||||
현재 `DROP` source enum은 full adapter를 의미하지 않는다. 선택 시 별도 inbound
|
||||
adapter가 `DataTransfer`를 event 안에서 snapshot하고, file-only drop과 directory
|
||||
traversal을 구분한다. pasted/dropped HTML, URL과 string item을 file capability로
|
||||
승격하지 않는다. same count/byte/type/path policy를 picker와 공유하되 UI event
|
||||
type을 permission으로 사용하지 않는다.
|
||||
|
||||
## 12. Range와 private cache는 별도 capability
|
||||
|
||||
### 12.1 Range/206
|
||||
|
||||
public static cache는 `Range` request와 206 response를 계속 거절한다. resumable
|
||||
download에는 별도 계약이 필요하다.
|
||||
|
||||
- immutable object version 또는 strong validator
|
||||
- `Range`/`If-Range`
|
||||
- exact `206 Content-Range`
|
||||
- `200`, `206`, `412`, `416` state transition
|
||||
- destination offset/seek/truncate와 partial checkpoint
|
||||
- overlap/gap 방지
|
||||
- capability 재발급 시 같은 representation binding
|
||||
- 전체 완료 뒤 whole-object integrity
|
||||
|
||||
sparse range를 Cache Storage entry로 합치는 것은 현재 public release port의 역할이
|
||||
아니다. 필요하면 OPFS staging 또는 별도 range store를 선택하고 backend
|
||||
File/Object Server와 validator/range 계약을 맞춘다.
|
||||
|
||||
### 12.2 private response cache
|
||||
|
||||
private/auth/account representation은 public Cache Storage adapter에서 계속
|
||||
fail-closed한다. offline private data가 제품 요구면 별도 설계가 최소 다음을
|
||||
소유해야 한다.
|
||||
|
||||
- current authorization과 server source-of-truth
|
||||
- opaque account partition
|
||||
- logout/account-deletion purge authority
|
||||
- TTL/revalidation/revocation
|
||||
- offline disclosure threat model
|
||||
- export/recovery
|
||||
- XSS가 same-origin key를 사용할 수 있다는 한계
|
||||
|
||||
client-side encryption만으로 authorization boundary를 만들었다고 주장하지 않는다.
|
||||
security/privacy 승인이 없으면 network-only다.
|
||||
|
||||
## 13. fault and recovery matrix
|
||||
|
||||
| fault | fail-closed 결과 | recovery |
|
||||
| --- | --- | --- |
|
||||
| estimate unavailable | `UNKNOWN` | essential만 policy budget 내 허용, speculative cache defer |
|
||||
| pressure/critical | admission 제한 | bounded GC, sync/export 안내 |
|
||||
| first quota failure | transaction/candidate rollback | eligible GC 후 exact operation 최대 1회 retry |
|
||||
| second quota failure | 신규 write 중지 | read-only/online-only/export-required |
|
||||
| partial sentinel mismatch | `STORAGE_EVICTED`/`CORRUPT_DATA` | reconstructable rehydrate, local-first quarantine |
|
||||
| 모든 local marker 소실 | first install과 구분 불가 | backend epoch가 있으면 비교, 없으면 정직한 degraded UX |
|
||||
| migration crash | old committed generation 유지 | checkpoint부터 resume/reconcile |
|
||||
| future schema | `INCOMPATIBLE` | N-1 destructive write 금지, online-only/read-only |
|
||||
| OPFS real probe fail | `DEGRADED/UNAVAILABLE` | async fallback probe 또는 online-only |
|
||||
| Cache cleanup deadline | partial success + cursor | 다음 bounded invocation |
|
||||
| corrupt active pointer | cleanup/interception 중지 | network-only, verified recovery tool |
|
||||
| preview pixel/decode limit | `LIMIT_EXCEEDED/POLICY_REJECTED` | attachment-only 또는 reselect |
|
||||
| persistent permission revoked | `PERMISSION_DENIED` | 명시적 reselect/re-authorize |
|
||||
| SW old/new incompatibility | activation 중지 | old active 유지 또는 verified previous 재활성화 |
|
||||
|
||||
## 14. observability
|
||||
|
||||
허용:
|
||||
|
||||
- capability/lifecycle/runtime-health 상태
|
||||
- operation과 closed failure code
|
||||
- pressure/byte/count/duration bucket
|
||||
- migration version ID와 processed/remaining bucket
|
||||
- GC deadline/more-available 여부
|
||||
- probe phase와 capability boolean
|
||||
- release registry ID처럼 registry-owned non-user identifier
|
||||
|
||||
금지:
|
||||
|
||||
- filename, directory path, object ID, account/tenant ID
|
||||
- URL/query/request/response body
|
||||
- exact digest, ETag, raw cache/DB/path name
|
||||
- handle, capability receipt, authority proof
|
||||
- native exception message/stack
|
||||
- exact usage/quota로 사용자의 device storage를 fingerprint하는 event
|
||||
|
||||
## 15. rollout and rollback
|
||||
|
||||
### 15.1 구현 순서
|
||||
|
||||
1. lifecycle policy/registry와 deterministic state machine
|
||||
2. bounded maintenance page/cursor 계약
|
||||
3. cross-store admission + injected fault adapters
|
||||
4. OPFS real preflight
|
||||
5. OPFS/Cache historical migration fixtures와 runtime
|
||||
6. preview safety probe
|
||||
7. optional capability는 제품 선택 후 별도 branch에서 구현
|
||||
|
||||
새 runtime은 구현과 evidence가 끝나도 skeleton에서는
|
||||
`AVAILABLE_NOT_COMPOSED`로 종료한다.
|
||||
|
||||
### 15.2 제품 승격
|
||||
|
||||
```text
|
||||
owner/policy와 필요한 경우 backend authority/re-sync 결정
|
||||
-> registry 및 immutable composition
|
||||
-> primaryStatus=COMPOSED, TrafficAdmission=DISABLED
|
||||
-> real browser readiness/shadow inspection
|
||||
-> RuntimeHealth=AVAILABLE 또는 승인된 DEGRADED
|
||||
-> PromotionEvidence=COMPLETE
|
||||
-> TrafficAdmission=CANARY (reconstructable dataset)
|
||||
-> TrafficAdmission=CANARY (user-authored write)
|
||||
-> migration/rollback drill
|
||||
-> TrafficAdmission=ENABLED
|
||||
```
|
||||
|
||||
user-authored/local-first를 reconstructable cache보다 먼저 canary하지 않는다.
|
||||
|
||||
### 15.3 rollback
|
||||
|
||||
1. 신규 write, migration, cache activation과 SW update를 disable한다.
|
||||
2. in-flight operation을 abort/drain하고 native truth를 reconcile한다.
|
||||
3. current schema를 읽을 수 있는 bundle은 read-only로 유지한다.
|
||||
4. N-1이 future schema면 online-only/export-required로 전환한다.
|
||||
5. previous verified static cache가 있으면 explicit activation으로 rollback한다.
|
||||
6. user-authored data는 sync/export 확인 없이 purge하지 않는다.
|
||||
7. old physical/cache generation은 rollback window와 client drain 뒤 bounded
|
||||
maintenance로 정리한다.
|
||||
|
||||
## 16. test and promotion evidence
|
||||
|
||||
### 16.1 deterministic tests
|
||||
|
||||
- threshold/hysteresis와 concurrent admission
|
||||
- pressure leader lock loss, abort, timeout
|
||||
- first quota failure -> GC -> exact one retry
|
||||
- retry가 non-idempotent/partial commit/second failure에서 차단됨
|
||||
- GC ordering과 user-authored non-eviction
|
||||
- sentinel partial mismatch와 all-marker-loss ambiguity
|
||||
- migration batch crash/resume/replay/fencing
|
||||
- OPFS v1->v2 copy/verify/publish/cleanup fault
|
||||
- Cache candidate failure, pointer corruption, rollback과 cursor expiry
|
||||
- cleanup/inspect count/deadline absolute ceiling
|
||||
- preview hostile dimensions, integer overflow, animation, truncated container와 decode
|
||||
- permission denied/revoked/stale persistent handle
|
||||
- redaction과 dependency snapshot mutation
|
||||
|
||||
### 16.2 real browser tests
|
||||
|
||||
Chromium, Firefox와 WebKit에서 지원 범위를 명시하고 skip을 success로 세지 않는다.
|
||||
|
||||
- StorageManager estimate/persist denial
|
||||
- native IndexedDB/OPFS/Cache quota exception mapping
|
||||
- DedicatedWorker + Web Lock + OPFS write/read/delete probe
|
||||
- two-tab migration/maintenance serialization
|
||||
- versionchange/future schema and N-1 read-only
|
||||
- actual Cache stage/activate/previous rollback/controlled client drain
|
||||
- storage clear 뒤 explicit degraded behavior
|
||||
- file preview real static decode/cleanup
|
||||
- directory/handle은 지원 engine + OS manual evidence
|
||||
|
||||
quota를 실제로 완전히 채우는 flaky test는 유일한 gate로 쓰지 않는다. deterministic
|
||||
fault injection과 실제 small-operation smoke를 함께 보존한다.
|
||||
|
||||
### 16.3 promotion artifact
|
||||
|
||||
artifact는 다음을 포함한다.
|
||||
|
||||
- release/commit, browser/OS/image
|
||||
- policy/registry/migration suite version과 hash
|
||||
- runtime lifecycle/health/admission
|
||||
- deterministic + native pass/fail/skip
|
||||
- historical fixture N-1/N/N+1 결과
|
||||
- rollback drill과 recovery runbook link
|
||||
- evidence expiry와 waiver
|
||||
|
||||
필수 engine skip, expired evidence, migration fixture 누락, quota retry invariant 위반,
|
||||
preview decode safety 누락 또는 user-authored auto-delete가 있으면 promotion을
|
||||
차단한다.
|
||||
|
||||
## 17. 완료 기준
|
||||
|
||||
이 결정의 공통 runtime 구현은 다음을 모두 만족해야
|
||||
`AVAILABLE_NOT_COMPOSED`로 완료된다.
|
||||
|
||||
- origin coordinator가 immutable registry, 다섯 primary status literal과 독립된
|
||||
selection/admission/health/evidence 축을 강제
|
||||
- pressure hysteresis, bounded GC와 exact one-retry가 executable test로 검증
|
||||
- all-marker-loss ambiguity를 API/result/문서에서 숨기지 않음
|
||||
- OPFS real preflight가 worker/lock/journal/write/read/delete/cleanup을 검증
|
||||
- OPFS/Cache forward migration과 N-1 rollback historical fixture 통과
|
||||
- Cache inspect/cleanup이 cursor/count/deadline 상한을 강제
|
||||
- static Cache와 optional Service Worker composition이 import/bundle 경계로 분리
|
||||
- preview가 pixel/decoded-byte/animation/decode limit을 object URL 전에 강제
|
||||
- directory/persistent/drop이 transient picker에 암묵적으로 추가되지 않음
|
||||
- Range download와 private/sparse Range cache가 서로도 별도 capability로
|
||||
남고 public cache가 둘을 계속 거부
|
||||
- Chromium/Firefox/WebKit의 required evidence와 recovery/rollback drill 완성
|
||||
- default production build에는 선택되지 않은 runtime, worker, DB open, listener,
|
||||
timer가 없음
|
||||
|
||||
이 기준 전에는 기존 `AVAILABLE_NOT_COMPOSED` runtime 일부가 존재하더라도 origin
|
||||
storage lifecycle 전체를 production-ready 또는 `COMPOSED`라고 부르지 않는다.
|
||||
@@ -0,0 +1,638 @@
|
||||
# VD-16: Browser transfer composition과 Image delivery
|
||||
|
||||
- 상태: Accepted — design complete, implementation pending
|
||||
- 결정일: 2026-07-28
|
||||
- 이 ADR이 선택한 common delta의 current status:
|
||||
`DESIGNED_NOT_IMPLEMENTED`
|
||||
- common delta의 목표 reference status:
|
||||
`AVAILABLE_NOT_COMPOSED`
|
||||
- 관련 결정: VD-10, VD-11, VD-12, VD-13, VD-14, VD-15
|
||||
- current status ledger:
|
||||
`docs/architecture/browser-data-capability-completion-ledger.md`
|
||||
- 재검토: 첫 product upload/download/Image CDN capability를 조합하기 전
|
||||
|
||||
## 배경
|
||||
|
||||
현재 저장소에는 File runtime, presigned capability provider/vault/executor,
|
||||
multipart/resumable upload, one-shot streaming download와 Image CDN verification
|
||||
runtime의 개별 factory가 있다. 이 구현들은 production bootstrap에서 제거돼
|
||||
있고 각각의 local `close()` 또는 `dispose()`만 제공한다.
|
||||
|
||||
제품에서 이들을 직접 조합하면 다음 문제가 생긴다.
|
||||
|
||||
- account/session이 바뀌어도 이전 capability, checkpoint, refresh 또는 async
|
||||
completion이 살아남을 수 있다.
|
||||
- 서로 다른 config가 같은 byte/resource/preset을 다르게 해석할 수 있다.
|
||||
- 일부 provider만 생성된 partial runtime이 요청을 받기 시작할 수 있다.
|
||||
- upload, download와 image에 retry, kill switch, deadline과 observation owner가
|
||||
중복될 수 있다.
|
||||
- Image engine은 이미 decode된 server descriptor를 받으므로 BFF transport,
|
||||
expiry refresh와 presentation handoff의 책임이 비어 있다.
|
||||
- 실제 provider가 frontend mock과 같은 계약을 지키는지 재사용 가능한
|
||||
conformance harness가 없다.
|
||||
|
||||
이 결정은 도메인별 upload 화면이나 cloud vendor를 공통 플랫폼에 넣지 않는다.
|
||||
선택된 capability를 안전하게 조립·폐기하는 composition protocol과 Image
|
||||
descriptor acquisition 경계를 정한다.
|
||||
|
||||
## 현재 상태
|
||||
|
||||
| 항목 | 상태 | 설명 |
|
||||
| --- | --- | --- |
|
||||
| 개별 presigned/upload/image runtime | `AVAILABLE_NOT_COMPOSED` | factory와 deterministic test가 있으나 제품 graph에는 없음 |
|
||||
| one-shot streaming download | `AVAILABLE_NOT_COMPOSED` | Range resume가 아닌 전체 객체 스트림 |
|
||||
| top-level transfer runtime | `DESIGNED_NOT_IMPLEMENTED` | export index만 있고 atomic factory/readiness/lifecycle 없음 |
|
||||
| Image descriptor HTTP provider | `DESIGNED_NOT_IMPLEMENTED` | caller가 `BackendIssuedImageAsset`을 직접 전달 |
|
||||
| safe image DOM projection | `DESIGNED_NOT_IMPLEMENTED` | presentation descriptor는 있으나 renderer boundary 없음 |
|
||||
| app-managed background download | `NOT_SELECTED` | VD-14의 별도 optional capability |
|
||||
| cross-browser background-download guarantee | `PLATFORM_LIMITED` | browser-managed handoff가 기본 fallback |
|
||||
| app-managed background upload | `NOT_SELECTED` | durable source staging/worker auth protocol이 별도로 필요 |
|
||||
| cross-browser background-upload guarantee | `PLATFORM_LIMITED` | worker lifetime/local source permission을 공통 보장할 수 없음 |
|
||||
|
||||
이 ADR을 추가해도 위 상태는 자동으로 바뀌지 않는다. port, runtime, test와
|
||||
removal evidence가 구현된 뒤에만 reference 상태를 올린다.
|
||||
|
||||
## 결정
|
||||
|
||||
### 1. 하나의 account-scoped composition owner
|
||||
|
||||
선택된 file/transfer/image capability는
|
||||
`BrowserTransferRuntimeComposition` 역할의 단일 owner가 다음 순서로 생성한다.
|
||||
|
||||
```text
|
||||
parse immutable config
|
||||
-> validate implementation ceilings
|
||||
-> obtain immutable session/account scope
|
||||
-> create policy registries
|
||||
-> create provider transports
|
||||
-> create capability vaults
|
||||
-> create checkpoint/lock/channel owners
|
||||
-> create file/upload/download/image runtimes
|
||||
-> run required compatibility probes
|
||||
-> publish READY facade atomically
|
||||
```
|
||||
|
||||
factory가 중간에 실패하면 생성된 owner를 역순으로 닫고 facade를 반환하지 않는다.
|
||||
partial runtime, degraded provider 또는 mutable config를 application에 노출하지
|
||||
않는다.
|
||||
|
||||
composition은 다음 두 종류를 반환하는 union이어야 한다.
|
||||
|
||||
```text
|
||||
READY {
|
||||
generation,
|
||||
capabilities,
|
||||
application facades,
|
||||
readiness,
|
||||
close()
|
||||
}
|
||||
|
||||
UNAVAILABLE {
|
||||
safe reason,
|
||||
retryability,
|
||||
fallback capability,
|
||||
disposePartial()
|
||||
}
|
||||
```
|
||||
|
||||
`UNAVAILABLE`에 provider URL, raw browser exception, account/tenant ID 또는
|
||||
credential을 넣지 않는다.
|
||||
|
||||
### 2. Runtime config는 closed schema다
|
||||
|
||||
config는 composition root만 읽고 깊은 snapshot/freeze한다. 최소한 다음
|
||||
registry-owned reference를 갖는다.
|
||||
|
||||
- config schema version과 runtime compatibility version
|
||||
- opaque session/account scope와 generation
|
||||
- application origin과 fixed BFF endpoint IDs
|
||||
- exact `PRESIGNED_TRANSFER_V1`, `PRESIGNED_MULTIPART_V1`,
|
||||
`RANGE_RESUMABLE_DOWNLOAD_V1`, `IMAGE_CDN_DESCRIPTOR_V1` 중 선택한 protocol
|
||||
registry와 fixed endpoint map
|
||||
- upload purpose/profile, part/concurrency/retry/deadline hard ceiling
|
||||
- download profile, size/integrity/strategy와 Range capability selection
|
||||
- checkpoint namespace, retention, inventory와 maintenance budget
|
||||
- lock/cancel transport selection과 unsupported outcome
|
||||
- Image issuer/origin/preset/key/probe/decode policy
|
||||
- descriptor refresh lead time, request/decode deadline와 concurrency
|
||||
- capability별 traffic admission과 kill switch
|
||||
- observation sink와 redaction policy
|
||||
- active-operation drain deadline
|
||||
|
||||
caller는 raw endpoint, URL, header, object key, transform, retry count, byte ceiling,
|
||||
cache policy 또는 account partition을 request마다 override할 수 없다.
|
||||
|
||||
config는 구현 절대 상한을 높일 수 없다. 구현 상한보다 큰 값, 중복 registry ID,
|
||||
same-origin private Image CDN, 모순되는 fallback 또는 provider 누락은 startup에서
|
||||
fail-closed한다.
|
||||
|
||||
### 3. Lifecycle state machine
|
||||
|
||||
top-level runtime은 다음 상태만 가진다.
|
||||
|
||||
```text
|
||||
CREATING
|
||||
-> PROBING
|
||||
-> READY
|
||||
-> DRAINING
|
||||
-> CLOSED
|
||||
|
||||
CREATING | PROBING
|
||||
-> FAILED
|
||||
-> CLOSED
|
||||
```
|
||||
|
||||
- `READY`만 새 operation을 받는다.
|
||||
- `DRAINING`은 새 operation을 거절하고 진행 중 operation에 bounded deadline을
|
||||
제공한다.
|
||||
- deadline 뒤 남은 operation은 runtime lifetime signal로 abort한다.
|
||||
- `close()`는 terminal/idempotent이며 `DRAINING/CLOSED`에서 반복 호출해도
|
||||
새 side effect를 만들지 않는다.
|
||||
- 닫힌 runtime은 reopen하지 않는다. 새 config/scope에는 새 generation을 만든다.
|
||||
- operation은 시작할 때 runtime generation과 account scope snapshot을 얻고
|
||||
모든 async boundary와 terminal commit 전에 다시 확인한다.
|
||||
- 늦게 끝난 fetch, hash, IndexedDB transaction, image verification 또는 decode가
|
||||
old generation이면 결과를 폐기하고 native resource를 닫는다.
|
||||
|
||||
### 4. Logout과 account/tenant switch
|
||||
|
||||
session owner notification이 authority다. BroadcastChannel, storage event,
|
||||
capability expiry 또는 page unload를 logout authority로 사용하지 않는다.
|
||||
|
||||
```text
|
||||
session owner announces local revoke
|
||||
-> traffic admission CLOSED
|
||||
-> runtime generation FENCED
|
||||
-> reject new operations
|
||||
-> signal active reads/fetch/backoff/probes
|
||||
-> bounded drain
|
||||
-> close capability and image vaults
|
||||
-> close cancel channels and release locks/connections
|
||||
-> apply checkpoint retention/purge policy with exact old scope
|
||||
-> dispose observations
|
||||
-> CLOSED
|
||||
-> construct new scope/runtime independently
|
||||
```
|
||||
|
||||
- old-scope purge와 new-scope open을 같은 transaction이나 facade에 섞지 않는다.
|
||||
- checkpoint가 crash 때문에 남아도 exact scope/policy binding이 다르면 새
|
||||
runtime이 읽지 못해야 한다.
|
||||
- presigned URL, signed headers와 Image private URL은 어떤 teardown record에도
|
||||
저장하지 않는다.
|
||||
- old account의 descriptor refresh, part completion과 download destination
|
||||
commit은 generation fence 뒤 성공으로 보고하지 않는다.
|
||||
- logout이 backend capability의 즉시 revoke를 보장하지 않는다. 강한 회수가
|
||||
필요하면 BFF가 revoke authority 또는 proxy/relay를 제공해야 한다.
|
||||
|
||||
### 5. Capability별 facade
|
||||
|
||||
application에는 top-level runtime 자체나 native adapter를 반환하지 않는다.
|
||||
composition은 설치된 feature에 필요한 좁은 facade만 주입한다.
|
||||
|
||||
```text
|
||||
FeatureUploadFacade
|
||||
-> select local file profile
|
||||
-> create/resume/pause/abort approved purpose
|
||||
|
||||
FeatureDownloadFacade
|
||||
-> request approved resource
|
||||
-> receive policy-selected handoff/save outcome
|
||||
|
||||
FeatureImageFacade
|
||||
-> request opaque asset/preset
|
||||
-> receive safe presentation descriptor
|
||||
```
|
||||
|
||||
feature는 `File`, `Blob`, `Response`, `ReadableStream`, `FileSystemHandle`,
|
||||
presigned URL, Image signature DTO, checkpoint store 또는 QueryClient를 받지 않는다.
|
||||
progress UI를 위한 observation도 bounded aggregate snapshot이며 transfer
|
||||
authority가 아니다.
|
||||
|
||||
## Upload lifecycle integration
|
||||
|
||||
### 6. Pause와 abort는 다르다
|
||||
|
||||
top-level upload facade가 향후 제공할 상태는 다음과 같다.
|
||||
|
||||
```text
|
||||
ACTIVE
|
||||
-> PAUSE_REQUESTED
|
||||
-> PAUSED
|
||||
-> RESUMING
|
||||
-> ACTIVE
|
||||
|
||||
ACTIVE | PAUSED
|
||||
-> ABORT_REQUESTED
|
||||
-> ABORT_PENDING
|
||||
-> ABORTED
|
||||
|
||||
ACTIVE
|
||||
-> COMPLETING
|
||||
-> QUARANTINED
|
||||
```
|
||||
|
||||
- pause는 새 part와 retry를 중지하고 현재 bounded native operation을 abort한 뒤
|
||||
non-authorizing checkpoint를 유지한다.
|
||||
- cross-context pause wire literal은 `RESUMABLE_UPLOAD_PAUSE_V1`이며
|
||||
`uploadKey`, exact scope/generation과 bounded message metadata만 운반한다.
|
||||
- durable `PAUSED`를 추가하는 checkpoint는 `schemaVersion: 2`다. v1
|
||||
`ACTIVE | ABORT_PENDING` reader/writer와 섞지 않고 old-writer drain,
|
||||
historical migration과 N-1 fail-closed를 증명한다.
|
||||
- abort는 server session authority와 reconcile한 뒤 terminal checkpoint를
|
||||
제거한다.
|
||||
- pause signal은 authority가 아니라 best-effort same-scope hint다. 수신자는
|
||||
exact upload key/scope/generation을 검증한다.
|
||||
- checkpoint inventory는 presigned URL이나 raw file path 없이 opaque upload
|
||||
key, safe state, age/byte/part bucket과 expiry만 반환한다.
|
||||
- inventory/list와 retention sweep은 count, cursor, deadline을 갖는다.
|
||||
- abandoned/expired checkpoint는 server status 또는 expiry policy와 CAS를
|
||||
확인한 뒤 bounded batch로 제거한다.
|
||||
- file 재선택 뒤 source fingerprint/size/media/part layout이 exact하게 맞지
|
||||
않으면 resume하지 않는다.
|
||||
|
||||
app-managed background upload는 이 state machine을 재사용할 수 있지만 page
|
||||
runtime의 pause/resume를 background 보장으로 표현하지 않는다.
|
||||
|
||||
## Download integration
|
||||
|
||||
### 7. Strategy selector
|
||||
|
||||
download strategy는 application caller가 지정하지 않고 immutable profile,
|
||||
resource delivery class, expected bytes, browser capability와 user activation을
|
||||
입력으로 하는 headless selector가 결정한다.
|
||||
|
||||
| 조건 | 결과 |
|
||||
| --- | --- |
|
||||
| save picker 지원, user activation 있음, large stream | `WHOLE_OBJECT_PICKER_STREAM` |
|
||||
| server-managed resource, picker 없음 또는 handoff가 제품 정책 | `BROWSER_MANAGED_HANDOFF` |
|
||||
| generated artifact가 approved Blob cap 이하 | `BOUNDED_OBJECT_URL` |
|
||||
| large generated artifact, picker 없음 | `SERVER_GENERATION_REQUIRED` 또는 `UNSUPPORTED` |
|
||||
| Range profile + seekable destination + provider contract | `RANGE_RESUMABLE_FOREGROUND` |
|
||||
| background download가 선택되고 지원되는 browser + owned staging | `APP_MANAGED_BACKGROUND_DOWNLOAD` |
|
||||
|
||||
selector는 capability probe와 actual invocation failure를 구분한다.
|
||||
`WHOLE_OBJECT_PICKER_STREAM`인데 picker가 없으면 request validation 오류가 아니라
|
||||
`UNSUPPORTED` 또는 승인 fallback이어야 한다. Safari/WebView/private mode의
|
||||
fallback도 동일 표에서 결정하며 user agent 문자열만으로 기능을 가정하지 않는다.
|
||||
|
||||
selector의 위 값은 application-level `DownloadExecutionPlan`이다. 현재
|
||||
file-delivery primitive와의 mapping은 다음처럼 닫는다.
|
||||
|
||||
| execution plan | 현재 adapter mapping |
|
||||
| --- | --- |
|
||||
| `WHOLE_OBJECT_PICKER_STREAM` | `DownloadStrategy=PROMPT_AND_STREAM` |
|
||||
| `BROWSER_MANAGED_HANDOFF` | source kind `BROWSER_MANAGED_RESOURCE` + `DownloadStrategy=BROWSER_MANAGED` |
|
||||
| `BOUNDED_OBJECT_URL` | `DownloadStrategy=BOUNDED_OBJECT_URL` |
|
||||
| `RANGE_RESUMABLE_FOREGROUND` | VD-14의 별도 future port; 현재 adapter에 mapping 금지 |
|
||||
| `APP_MANAGED_BACKGROUND_DOWNLOAD` | 별도 optional worker/staging port |
|
||||
| `SERVER_GENERATION_REQUIRED` / `UNSUPPORTED` | browser delivery를 시작하지 않는 closed outcome |
|
||||
|
||||
Range resume의 checkpoint, validator와 seek/truncate 결정은 VD-14를 따른다.
|
||||
app-managed background download는 기본 selector 결과가 아니다.
|
||||
|
||||
## Image descriptor acquisition과 delivery
|
||||
|
||||
### 8. Wire protocol
|
||||
|
||||
private Image descriptor BFF 계약은 다음 literal을 사용한다.
|
||||
|
||||
```text
|
||||
IMAGE_CDN_DESCRIPTOR_V1
|
||||
```
|
||||
|
||||
request는 composition-owned fixed HTTPS endpoint를 호출하며 최소한 다음
|
||||
application-safe 입력만 허용한다.
|
||||
|
||||
- protocol
|
||||
- opaque asset reference
|
||||
- named preset reference 또는 preset family
|
||||
- intended presentation class
|
||||
- current runtime generation에 묶인 CSRF/session transport
|
||||
|
||||
caller는 CDN URL, source URL, origin, object key, width, height, DPR, quality, fit,
|
||||
format, cache header, signing key ID나 expiry를 제출하지 않는다.
|
||||
|
||||
response decoder는 content type, status, header/body byte cap, total deadline와
|
||||
closed JSON shape를 검증한다. response에는 최소한 다음이 binding된다.
|
||||
|
||||
- exact protocol과 issuer
|
||||
- opaque asset ID와 immutable revision
|
||||
- origin/preset binding IDs
|
||||
- static raster media와 intrinsic dimensions
|
||||
- allowed preset binding ID set
|
||||
- issued/expiry time
|
||||
- signature algorithm, key ID, canonical binding digest와 signature
|
||||
|
||||
unknown field 정책은 protocol version에서 고정한다. credential, backend stack,
|
||||
raw provider key 또는 arbitrary transform은 descriptor에 포함하지 않는다.
|
||||
|
||||
HTTP `200`만 descriptor success다. `401/403/404`의 외부 mapping은 existence
|
||||
hiding 정책에 따라 closed failure로 정규화하며 raw backend message를 버린다.
|
||||
redirect, opaque response, wrong content type, oversize, timeout와 malformed
|
||||
descriptor는 capability를 생성하지 않는다.
|
||||
|
||||
### 9. Provider와 verifier 경계
|
||||
|
||||
- BFF는 authorization, asset existence, quarantine/promotion state와 descriptor
|
||||
발급 authority를 소유한다.
|
||||
- verifier registry는 composition이 승인한 bounded old/new public key set만
|
||||
가진다.
|
||||
- client signature 검증은 BFF authorization의 대체가 아니라 response tamper와
|
||||
registry mismatch를 fail-closed하는 보조 경계다.
|
||||
- CDN은 asset revision과 preset binding ID로 exact transform candidate를
|
||||
재계산한다. signed query나 client 계산 width가 authority가 아니다.
|
||||
- private asset의 emergency revocation은 backend/CDN/BFF가 소유한다. client는
|
||||
runtime close와 short expiry로 exposure를 줄인다.
|
||||
|
||||
### 10. Refresh state machine
|
||||
|
||||
descriptor provider는 asset/preset/scope/generation별 bounded single-flight만
|
||||
허용한다.
|
||||
|
||||
```text
|
||||
ABSENT
|
||||
-> FETCHING
|
||||
-> VERIFIED
|
||||
-> FRESH
|
||||
-> REFRESH_DUE
|
||||
-> REFRESHING
|
||||
-> FRESH
|
||||
|
||||
FETCHING | REFRESHING
|
||||
-> TERMINAL_POLICY_FAILURE
|
||||
-> PLACEHOLDER
|
||||
|
||||
FETCHING | REFRESHING
|
||||
-> RETRYABLE_FAILURE
|
||||
-> EXISTING_FRESH_UNTIL_EXPIRY | PLACEHOLDER
|
||||
|
||||
any state + scope/runtime revoke
|
||||
-> REVOKED
|
||||
```
|
||||
|
||||
- refresh lead time은 config가 정하되 expiry hard ceiling을 넘지 않는다.
|
||||
- private descriptor를 generic Query cache, Web Storage 또는 IndexedDB에
|
||||
persistence하지 않는다.
|
||||
- concurrent callers는 같은 verified result를 받을 수 있지만 URL/string을
|
||||
application state에 장기 복사하지 않는다.
|
||||
- 기존 descriptor가 아직 fresh하고 refresh가 일시 실패하면 expiry까지만
|
||||
사용할 수 있다. expiry 뒤 stale-while-error를 금지한다.
|
||||
- `lazy` load로 실제 fetch가 expiry 뒤 시작될 가능성이 있으면 eager/priority로
|
||||
바꾸거나 load 직전에 새 descriptor를 발급한다.
|
||||
- logout, key registry replacement와 runtime generation 변경은 in-flight
|
||||
transport, verification과 probe를 abort하고 늦은 결과를 폐기한다.
|
||||
|
||||
### 11. Safe presentation projection
|
||||
|
||||
공통 presentation primitive는 검증된 `ImagePresentationDescriptor`를 다음
|
||||
정적 속성으로만 투영한다.
|
||||
|
||||
- fallback `src`
|
||||
- ordered `<source type srcset>`
|
||||
- registry-owned `sizes`
|
||||
- intrinsic `width`와 `height`
|
||||
- `loading`, `decoding`, `fetchpriority`
|
||||
- `referrerpolicy`
|
||||
- `crossorigin="anonymous"`
|
||||
|
||||
primitive는 URL을 parse·조립·append하거나 transform query를 생성하지 않는다.
|
||||
descriptor가 가진 string을 React property로 전달하기 전에 closed allowed
|
||||
protocol/origin과 runtime generation을 다시 확인한다. raw HTML 주입과 CSS URL
|
||||
조립을 금지한다.
|
||||
|
||||
다음은 제품 presentation owner가 결정한다.
|
||||
|
||||
- 의미 있는 `alt`
|
||||
- placeholder와 오류 copy
|
||||
- skeleton/aspect-ratio UX
|
||||
- above-the-fold preload/priority
|
||||
- route/SSR preload hint
|
||||
- click/open/download behavior
|
||||
|
||||
descriptor refresh 실패를 native broken-image UI에만 맡기지 않고 제품이 승인한
|
||||
placeholder outcome으로 매핑한다.
|
||||
|
||||
## Provider contract harness
|
||||
|
||||
### 12. 재사용 가능한 suite
|
||||
|
||||
frontend는 transport 구현과 분리된 provider contract harness를 제공한다. 같은
|
||||
case set을 deterministic fake, local emulator와 실제 BFF/object storage/CDN에
|
||||
실행한다.
|
||||
|
||||
Presigned/download case:
|
||||
|
||||
- exact protocol/status/content type/body cap
|
||||
- method/origin/path/query/header binding
|
||||
- redirect와 credential omission
|
||||
- expiry/revocation/replay
|
||||
- truncation/overrun/content encoding
|
||||
- CORS exposed receipt/checksum
|
||||
|
||||
Multipart case:
|
||||
|
||||
- create/status/part/complete/abort idempotency
|
||||
- part layout/checksum/receipt reconciliation
|
||||
- 404/410/expiry와 orphan cleanup
|
||||
- quarantine/promotion
|
||||
- retry-after와 ambiguous completion
|
||||
|
||||
Image case:
|
||||
|
||||
- protocol/issuer/key/preset exact match
|
||||
- old/new signing key overlap과 removal
|
||||
- immutable revision/cache key
|
||||
- private no-store/CORS/CSP
|
||||
- pixel/decode/encoded byte ceiling
|
||||
- malformed/animated/active content
|
||||
- expiry refresh, revocation과 placeholder
|
||||
|
||||
actual provider test는 bearer URL, signature, account/asset/session ID를 artifact에
|
||||
기록하지 않는다. fixture는 synthetic opaque values와 disposable storage를 쓴다.
|
||||
|
||||
## Failure, readiness와 fallback
|
||||
|
||||
### 13. Readiness report
|
||||
|
||||
readiness는 application-safe capability별 결과다.
|
||||
|
||||
| 상태 | 의미 |
|
||||
| --- | --- |
|
||||
| `READY` | 필수 provider/config/browser probe가 모두 유효 |
|
||||
| `DEGRADED` | 승인된 좁은 fallback만 가능 |
|
||||
| `UNAVAILABLE` | 기능을 노출하지 않음 |
|
||||
| `DRAINING` | 기존 작업만 정리 중 |
|
||||
| `CLOSED` | terminal |
|
||||
|
||||
`DEGRADED`는 byte/pixel/security ceiling을 낮출 수는 있지만 높이지 않는다.
|
||||
예를 들어 enhanced picker off → native input, multipart concurrency off →
|
||||
sequential, private Image CDN off → approved placeholder는 가능하다.
|
||||
integrity off, arbitrary URL 허용, private response cache 또는 unbounded Blob은
|
||||
fallback이 아니다.
|
||||
|
||||
### 14. Kill switch
|
||||
|
||||
최소한 다음 switch를 독립적으로 둔다.
|
||||
|
||||
- new presigned issuance
|
||||
- direct object-storage data plane
|
||||
- new upload session
|
||||
- upload resume
|
||||
- upload complete
|
||||
- Range resume
|
||||
- picker streaming save
|
||||
- private image descriptor issuance
|
||||
- advanced image format
|
||||
- responsive candidates
|
||||
|
||||
switch 변경은 active operation의 의미를 소급 변경하지 않는다. 신규 진입을
|
||||
닫은 뒤 reconcile/drain한다. remote runtime config를 사용한다면 config의
|
||||
authenticity, release compatibility와 last-known-safe 정책을 별도 hosting 계약으로
|
||||
검증한다.
|
||||
|
||||
## 관측성과 개인정보
|
||||
|
||||
허용:
|
||||
|
||||
- operation kind와 safe outcome
|
||||
- runtime/readiness state
|
||||
- byte/part/candidate/retry/age/deadline bucket
|
||||
- policy rejection, abort, reconcile와 drain bucket
|
||||
- aggregate active count, checkpoint count와 orphan age
|
||||
|
||||
금지:
|
||||
|
||||
- URL, query, signed/request/response header
|
||||
- capability, bearer token, signature와 key material
|
||||
- resource/session/asset/upload/account/tenant ID
|
||||
- file name, local path, object key와 storage physical key
|
||||
- digest, ETag, receipt와 checkpoint payload
|
||||
- raw backend/browser exception message와 stack
|
||||
|
||||
runtime generation과 registry ID도 외부 telemetry에 그대로 보내지 않고 bounded
|
||||
compatibility bucket으로 변환한다.
|
||||
|
||||
## Rollout과 rollback
|
||||
|
||||
### 15. Rollout
|
||||
|
||||
1. ledger와 관련 ADR을 accepted로 고정한다.
|
||||
2. closed config/port와 provider contract harness를 먼저 구현한다.
|
||||
3. fake와 negative fixture에서 partial composition/late result를 거절한다.
|
||||
4. top-level runtime을 `AVAILABLE_NOT_COMPOSED`로 유지하고 removal gate를 만든다.
|
||||
5. product owner, exact scope와 provider config를 선택하고 bootstrap에 kill
|
||||
switch `DISABLED` 상태로 조합한다. 이 시점 primary status는 `COMPOSED`다.
|
||||
6. 실제 BFF/storage/CDN conformance와 readiness probe를 통과한다.
|
||||
7. Chromium/Firefox/WebKit과 실제 device에서 account switch, expiry와 crash를
|
||||
검증한다.
|
||||
8. runbook/rollback drill 뒤 internal cohort의 read-only/image public 또는 upload
|
||||
shadow flow부터 연다.
|
||||
9. private/image upload/download capability를 독립 canary와 kill switch로 확대한다.
|
||||
|
||||
### 16. Rollback
|
||||
|
||||
1. 신규 issuance/session/descriptor를 중지한다.
|
||||
2. runtime을 `DRAINING`으로 바꾸고 bounded operation을 마무리한다.
|
||||
3. ambiguous upload는 server reconcile하고 Range partial은 checkpoint 정책대로
|
||||
보존 또는 삭제한다.
|
||||
4. private capability를 backend에서 revoke하고 client runtime을 close한다.
|
||||
5. old compatible composition을 새 generation으로 다시 생성하거나 기능을
|
||||
unavailable로 유지한다.
|
||||
6. optional source, config와 facade를 제거하고 production module inventory와
|
||||
removal gate를 재검증한다.
|
||||
|
||||
schema version을 내리거나 checkpoint를 무조건 삭제해 rollback하지 않는다.
|
||||
|
||||
## 검증과 완료 기준
|
||||
|
||||
### 17. Deterministic
|
||||
|
||||
- partial factory failure의 reverse-order cleanup
|
||||
- close idempotency와 closed-runtime rejection
|
||||
- account switch 중 late fetch/hash/transaction/decode drop
|
||||
- config duplicate/ceiling/missing provider fail-closed
|
||||
- selector의 picker/size/resource matrix
|
||||
- upload pause/abort/reconcile race와 bounded inventory
|
||||
- descriptor refresh single-flight, expiry와 generation fence
|
||||
- picture projection의 arbitrary URL/query 생성 0건
|
||||
- diagnostics forbidden-value negative fixtures
|
||||
|
||||
### 18. Native browser
|
||||
|
||||
- Chromium/Firefox/WebKit의 native input/save picker fallback
|
||||
- multi-tab upload pause/cancel과 unsupported Web Locks path
|
||||
- page reload/account switch 중 active transfer drain
|
||||
- public/private Image fetch, actual CORS/no-store와 bitmap decode
|
||||
- offline/timeout/abort/late completion cleanup
|
||||
- Safari/WebView/private mode의 approved selector result
|
||||
|
||||
### 19. Provider와 operations
|
||||
|
||||
- fake/emulator/실제 BFF·object storage·CDN 동일 contract suite
|
||||
- signing key rotation과 emergency revoke drill
|
||||
- checkpoint retention/orphan cleanup drill
|
||||
- capability별 kill switch와 N-1 rollback
|
||||
- runtime removal 후 source-module inventory 0건
|
||||
|
||||
다음 조건 전에는 목표 상태를 `AVAILABLE_NOT_COMPOSED`로 올리지 않는다.
|
||||
|
||||
- [ ] top-level closed config와 atomic factory가 구현됐다.
|
||||
- [ ] generation-bound lifecycle과 account teardown이 구현됐다.
|
||||
- [ ] `RESUMABLE_UPLOAD_PAUSE_V1`, checkpoint schema v2 old-writer
|
||||
migration과 bounded upload inventory/retention owner가 구현됐다.
|
||||
- [ ] `PRESIGNED_TRANSFER_V1`과 선택 protocol의 strict codec, unknown-version
|
||||
rejection 및 reusable provider contract harness가 구현됐다.
|
||||
- [ ] strategy selector가 browser fallback을 fail-closed한다.
|
||||
- [ ] Image descriptor provider/refresh와 safe projection이 구현됐다.
|
||||
- [ ] deterministic fault, boundary, removal test가 통과한다.
|
||||
|
||||
다음 조건 전에는 제품 상태를 `COMPOSED`로 올리지 않는다.
|
||||
|
||||
- [ ] product owner와 opaque account scope가 정해졌다.
|
||||
- [ ] strict config를 사용하는 top-level runtime이 production bootstrap에서
|
||||
생성되고 실제 feature facade consumer까지 연결됐다.
|
||||
- [ ] traffic 기본값이 `DISABLED`이며 local teardown/close 경로가 연결됐다.
|
||||
|
||||
다음 조건 전에는 product-local production traffic을 승인하지 않는다.
|
||||
|
||||
- [ ] actual BFF/storage/CDN contract harness가 통과한다.
|
||||
- [ ] browser/device evidence와 runbook drill이 보존됐다.
|
||||
- [ ] kill switch와 rollback owner가 운영 승인됐다.
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [Browser data capability completion ledger](../browser-data-capability-completion-ledger.md)
|
||||
- [Presigned transfer and Image CDN](../presigned-transfer-and-image-cdn.md)
|
||||
- [VD-14 Resumable download와 background download](./VD-14-resumable-download-and-background-transfer.md)
|
||||
- [Server file capability infrastructure](../server-file-capability-infrastructure.md)
|
||||
- [Browser transfer recovery](../../operations/browser-transfer-recovery.md)
|
||||
|
||||
## 선택하지 않은 대안
|
||||
|
||||
- feature가 개별 transfer adapter factory를 직접 조합
|
||||
- singleton runtime을 여러 account/tenant가 공유
|
||||
- raw presigned/Image URL을 Query cache나 persistence에 저장
|
||||
- Image descriptor endpoint/transform을 request마다 caller가 지정
|
||||
- page abort를 upload pause 또는 server abort 완료로 간주
|
||||
- browser-managed handoff를 저장 완료로 간주
|
||||
- user-agent 문자열만으로 Safari fallback 결정
|
||||
- Service Worker를 설치하면 background upload/download가 보장된다고 가정
|
||||
- capability 일부만 준비된 partial runtime을 degraded success로 반환
|
||||
|
||||
## 결과
|
||||
|
||||
장점:
|
||||
|
||||
- account 전환과 teardown의 한 owner가 생긴다.
|
||||
- 개별 runtime의 안전한 메커니즘을 제품별 facade로 좁혀 조합할 수 있다.
|
||||
- provider mock과 실제 인프라 사이의 계약 차이를 같은 suite로 찾을 수 있다.
|
||||
- Image URL과 transform이 application/presentation에서 재조립되지 않는다.
|
||||
- capability별 rollout, kill switch와 제거가 독립적이다.
|
||||
|
||||
비용:
|
||||
|
||||
- config, lifecycle, provider harness와 browser evidence가 늘어난다.
|
||||
- 제품이 선택하지 않은 capability는 여전히 조합할 수 없으며 이것이 의도된
|
||||
결과다.
|
||||
- actual provider와 운영 증거 없이는 reference runtime 구현만으로 production
|
||||
완료를 주장할 수 없다.
|
||||
@@ -0,0 +1,729 @@
|
||||
# VD-23: API transport selection과 REST execution
|
||||
|
||||
- 상태: Accepted — REST v2 security/execution baseline composed, advanced profiles pending
|
||||
- 결정일: 2026-07-28
|
||||
- installed REST reference vertical: `COMPOSED`
|
||||
- REST v2 security/execution baseline: `COMPOSED`
|
||||
- provider/path/auth profile baseline: `COMPOSED`
|
||||
- conditional execution/provider-conformance delta: `DESIGNED_NOT_IMPLEMENTED`
|
||||
- GraphQL/Connect/gRPC-Web/REST Gateway product selection: `NOT_SELECTED`
|
||||
- 관련 결정: VD-13, VD-24, VD-25, VD-26, VD-27, VD-29, VD-30
|
||||
- 상세 설계:
|
||||
[API contract, Schema, Mapper와 Server State](../api-contract-schema-mapper-and-server-state.md)
|
||||
- browser Protobuf/gateway 설계:
|
||||
[Protobuf browser transport와 REST Gateway](../protobuf-browser-transport-and-rest-gateway.md)
|
||||
|
||||
## 배경
|
||||
|
||||
현재 reference feature는 operation registry, request Zod schema, shared HTTP
|
||||
executor, response envelope/payload schema, mapper, gateway, application input과
|
||||
TanStack Query까지 실제 production composition에 연결된다. 따라서 REST 경로
|
||||
자체를 미구현으로 표시하지 않는다.
|
||||
|
||||
현재 reference operation은 v2 metadata, collision-aware composition, allowlisted
|
||||
credential patch, auth fail-before-fetch, prefix-preserving target, shared logical
|
||||
deadline, exact JSON media/success status, bounded decoder와 typed mapping failure를
|
||||
실행한다. correlation header, success status group과 401 replay를 포함한 physical
|
||||
attempt도 terminal observation에 연결한다.
|
||||
|
||||
operation은 path placeholder↔codec key exact join, named provider/bearer/CSRF
|
||||
profile, provider credential-mode ceiling과 encoded query byte ceiling도 실행한다.
|
||||
남은 production delta는 cookie-CSRF/CORS evidence, 204/304/412 conditional
|
||||
transaction, artifact digest와 실제 provider conformance다.
|
||||
`API_CONTRACT_VERSION` 문자열 일치만 backend compatibility 증거로 사용하지 않는다.
|
||||
|
||||
## 결정
|
||||
|
||||
### 1. Protocol 선택은 operation registry가 소유한다
|
||||
|
||||
feature application port는 protocol-neutral이다.
|
||||
|
||||
```text
|
||||
feature use case
|
||||
-> feature gateway
|
||||
-> exact semantic operation
|
||||
-> REST adapter
|
||||
-> persisted GraphQL adapter
|
||||
-> Connect adapter
|
||||
-> gRPC-Web adapter
|
||||
```
|
||||
|
||||
- 한 `operationId`는 한 protocol에만 binding한다.
|
||||
- UI, query hook과 use case는 protocol을 선택하지 않는다.
|
||||
- URL, GraphQL document, service/method와 generated request를 application input으로
|
||||
받지 않는다.
|
||||
- 같은 command를 provider failure 때문에 다른 protocol로 자동 replay하지 않는다.
|
||||
- read fallback도 동일 auth/freshness/schema/mapper/query identity와 하나의 total
|
||||
retry budget을 증명한 registered policy만 허용한다.
|
||||
- GraphQL/Connect/gRPC-Web adapter를 공통 `HttpClient`의 mode flag로 넣지 않는다.
|
||||
공통으로 공유하는 것은 execution context, auth collaboration, failure vocabulary와
|
||||
observation뿐이다.
|
||||
|
||||
### 2. REST operation은 실행 source와 정적 manifest를 분리하지 않는다
|
||||
|
||||
목표 API는 feature가 generic이 연결된 definition을 만든다.
|
||||
|
||||
```text
|
||||
defineRestOperation<
|
||||
Path,
|
||||
Search,
|
||||
Body,
|
||||
SuccessWire,
|
||||
SuccessValue,
|
||||
Failure
|
||||
>({
|
||||
common,
|
||||
providerId,
|
||||
method,
|
||||
uriTemplate,
|
||||
pathCodec,
|
||||
searchCodec,
|
||||
bodyCodec,
|
||||
successCodec,
|
||||
errorCodec,
|
||||
mapper,
|
||||
policies
|
||||
})
|
||||
```
|
||||
|
||||
실행 definition에서 registry manifest를 결정적으로 투영한다. schema metadata,
|
||||
실제 codec map, operation-specific TypeScript map과 mapper를 서로 다른 string
|
||||
dispatch table에 수기로 중복하지 않는다.
|
||||
|
||||
manifest 최소 field:
|
||||
|
||||
```text
|
||||
protocol = REST
|
||||
registryVersion
|
||||
providerId
|
||||
operationId
|
||||
owner
|
||||
semantics
|
||||
method
|
||||
relativePathTemplate
|
||||
pathSchemaId
|
||||
searchSchemaId
|
||||
bodySchemaId
|
||||
requestMediaProfile
|
||||
successStatusProfiles[]
|
||||
errorStatusProfiles[]
|
||||
responseSchemaId
|
||||
mapperId
|
||||
authProfileId
|
||||
csrfProfileId
|
||||
replayPolicy
|
||||
idempotencyKeyPolicy
|
||||
deadlineProfileId
|
||||
retryProfileId
|
||||
paginationProfileId | null
|
||||
conditionalProfileId
|
||||
serverStateProfileId | null
|
||||
maxRequestBytes
|
||||
maxDecodedResponseBytes
|
||||
maxResponseItems
|
||||
observabilityProfileId
|
||||
compatibility
|
||||
```
|
||||
|
||||
### 3. Contribution composer는 collision 전에 실패한다
|
||||
|
||||
```text
|
||||
feature contributions[]
|
||||
-> preserve every source row
|
||||
-> validate contribution owner/version
|
||||
-> detect duplicate operation/schema/mapper/query/topic ID
|
||||
-> resolve every codec/mapper/profile reference
|
||||
-> validate semantic coherence
|
||||
-> freeze installed registry
|
||||
-> emit compatibility manifest
|
||||
```
|
||||
|
||||
object spread의 last-write-wins를 금지한다. duplicate ID가 payload까지 동일해도
|
||||
owner를 하나 선택하지 않고 build를 실패시킨다. alias/rename은 versioned migration
|
||||
row로만 허용한다.
|
||||
|
||||
semantic coherence:
|
||||
|
||||
- `GET`/`HEAD`는 body 없음, `SAFE`만 허용
|
||||
- `HEAD`는 body success codec 없음
|
||||
- `POST`/`PATCH` retry는 `IDEMPOTENT | KEYED_COMMAND` 또는 명시적
|
||||
`SAFE` semantics 필요
|
||||
- `KEYED_COMMAND`는 `idempotencyKeyPolicy=REQUIRED`, 다른 replay policy의 key
|
||||
attach/금지는 exact key profile과 일치
|
||||
- `NON_REPLAYABLE`은 retry와 401 replay 금지
|
||||
- success/error status가 겹치지 않음
|
||||
- 204 profile은 body/schema 없음
|
||||
- 304는 query + conditional profile + existing cache binding 필요
|
||||
- 412는 precondition profile 필요
|
||||
- path placeholder 집합과 path codec key 집합이 exact match
|
||||
- query/cache profile은 `QUERY` operation에만 연결
|
||||
- mutation invalidation은 registered topic만 사용
|
||||
|
||||
### 4. Provider endpoint와 URI
|
||||
|
||||
application은 provider URL을 받지 않는다.
|
||||
|
||||
```text
|
||||
RestProviderProfile
|
||||
providerId
|
||||
baseOrigin
|
||||
basePathPrefix
|
||||
allowedCredentialsModes
|
||||
corsProfile
|
||||
referrerPolicy
|
||||
redirectPolicy = ERROR
|
||||
defaultHeaders
|
||||
allowedResponseOrigins
|
||||
```
|
||||
|
||||
- non-local은 HTTPS만 허용한다.
|
||||
- provider는 credential mode ceiling만 제공하고 operation의 auth transport
|
||||
profile이 exact mode를 선택한다. anonymous는 `omit`, same-origin cookie는
|
||||
approved `same-origin`, approved cross-origin cookie만 `include`다.
|
||||
- base URL의 username, password, query와 fragment를 금지한다.
|
||||
- exact origin과 canonical base path prefix를 보존한다.
|
||||
- operation template은 relative API path이며 scheme, authority, query와 fragment를
|
||||
포함하지 않는다.
|
||||
- leading slash가 base prefix를 제거하는 `new URL()` ambiguity를 쓰지 않는다.
|
||||
- encoded slash, dot segment, NUL/control, duplicate slash와 overlong path를
|
||||
정책대로 거절한다.
|
||||
- redirect는 기본 `error`다. 로그인/다운로드 handoff는 일반 JSON REST operation과
|
||||
다른 capability다.
|
||||
|
||||
path parameter:
|
||||
|
||||
- path codec의 parsed output만 encode한다.
|
||||
- missing, extra, empty와 length 초과 parameter를 network 전에 거절한다.
|
||||
- Unicode normalization을 업무 ID에 임의 적용하지 않는다.
|
||||
- path value와 최종 URL은 diagnostics에 기록하지 않는다.
|
||||
|
||||
query:
|
||||
|
||||
- key ordering, repeated-array/comma style, boolean, null/absent/empty semantics를
|
||||
profile에 고정한다.
|
||||
- URL encoded byte ceiling을 적용한다.
|
||||
- raw `URLSearchParams`, query string과 next URL을 caller에게 받지 않는다.
|
||||
- sensitive/private value를 GET query에 넣는 operation은 별도 security review가
|
||||
없으면 금지한다.
|
||||
|
||||
### 5. Request projection과 final invariant
|
||||
|
||||
request는 다음 소유 순서로 만든다.
|
||||
|
||||
```text
|
||||
operation + validated input
|
||||
-> immutable request binding
|
||||
-> body canonical serialization + digest
|
||||
-> transport-owned headers/options
|
||||
-> constrained auth/CSRF patch
|
||||
-> final invariant validation
|
||||
-> fetch
|
||||
```
|
||||
|
||||
transport-owned header:
|
||||
|
||||
- `Accept`, `Content-Type`
|
||||
- contract/media version
|
||||
- bounded correlation/trace context
|
||||
- idempotency key
|
||||
- conditional validator
|
||||
- approved CSRF header
|
||||
|
||||
caller와 feature mapper가 arbitrary header를 추가하지 않는다.
|
||||
|
||||
auth owner target:
|
||||
|
||||
```text
|
||||
CredentialPatch
|
||||
credentialMode
|
||||
allowlisted header name/value
|
||||
proof expiry/generation
|
||||
```
|
||||
|
||||
operation auth profile은 final Fetch `credentials`를 exact하게 고정한다.
|
||||
`ANONYMOUS | BEARER_HEADER`는 ambient cookie가 섞이지 않게 `omit`,
|
||||
same-origin cookie session은 `same-origin`, cross-origin cookie는 별도 CORS/CSRF
|
||||
provider evidence가 있는 profile만 `include`다. provider ceiling과 맞지 않으면
|
||||
fetch 0회다.
|
||||
|
||||
auth owner가 `Request` 전체를 반환하지 않는다. transition 기간에 current port를
|
||||
사용한다면 attach 전후의 다음 값이 exact하게 같아야 한다.
|
||||
|
||||
- URL/origin/path/query
|
||||
- method
|
||||
- body digest
|
||||
- content type/length
|
||||
- correlation, idempotency, conditional와 CSRF binding
|
||||
- redirect/cache/referrer/credentials/mode
|
||||
|
||||
다르면 `AUTH_INTEGRATION_FAILURE`, fetch 0회다.
|
||||
|
||||
auth-required operation은 session state가 `authenticated`가 아니면 fetch하지 않는다.
|
||||
`integration-failed`, `unauthenticated`와 credential attach rejection을 anonymous
|
||||
request로 downgrade하지 않는다.
|
||||
|
||||
### 6. Cookie auth, CSRF와 CORS
|
||||
|
||||
same-origin BFF cookie session을 기본 권장한다.
|
||||
|
||||
- cookie는 Secure/HttpOnly이며 provider가 SameSite 정책을 소유한다.
|
||||
- unsafe method는 server의 exact Origin/Fetch Metadata 검증과 composition-issued
|
||||
anti-CSRF proof를 요구한다.
|
||||
- CSRF proof는 application/query/cache에 노출하거나 persistence하지 않는다.
|
||||
- custom content type/preflight가 있다는 사실만 CSRF 방어로 간주하지 않는다.
|
||||
|
||||
cross-origin profile은 다음을 actual provider에서 증명한다.
|
||||
|
||||
- exact `Access-Control-Allow-Origin`, wildcard 금지
|
||||
- credentials mode와 allow-credentials 일치
|
||||
- exact allow-method/allow-header
|
||||
- 필요한 request ID, ETag, Retry-After만 expose
|
||||
- OPTIONS와 actual response의 policy 동등성
|
||||
- redirect 없음
|
||||
|
||||
### 7. Replay와 idempotency
|
||||
|
||||
```text
|
||||
ReplayPolicy
|
||||
SAFE
|
||||
IDEMPOTENT
|
||||
KEYED_COMMAND
|
||||
NON_REPLAYABLE
|
||||
```
|
||||
|
||||
- `SAFE`: read-only이며 network/recovery retry 가능
|
||||
- `IDEMPOTENT`: 같은 principal/operation/payload/precondition의 반복 request가
|
||||
의도한 server effect를 추가로 만들지 않으며 duplicate response/status mapping을
|
||||
backend contract가 명시한다. response byte가 항상 동일하다는 뜻은 아니다.
|
||||
- `KEYED_COMMAND`: application logical command lease가 key를 생성하고 lifecycle
|
||||
전체에서 유지
|
||||
- `NON_REPLAYABLE`: ambiguous result에서 자동 재실행 금지
|
||||
|
||||
`KEYED_COMMAND` binding:
|
||||
|
||||
```text
|
||||
principal scope
|
||||
operation ID/version
|
||||
canonical payload digest
|
||||
idempotency key
|
||||
server retention/expiry
|
||||
```
|
||||
|
||||
backend는 atomic claim, concurrent same-key join/replay, same-key different-payload
|
||||
rejection과 terminal receipt를 제공한다. header를 보냈다는 사실만 replay safety가
|
||||
아니다. provider conformance가 없으면 retry/401 recovery traffic을 켜지 않는다.
|
||||
|
||||
current client가 execute 호출마다 key를 생성하는 방식은 network attempt 안에서는
|
||||
재사용되지만 ambiguous terminal 뒤 사용자 retry와 연결되지 않는다. 목표 command
|
||||
owner가 effect certainty를 다음처럼 반환한다.
|
||||
|
||||
```text
|
||||
NOT_APPLIED | COMMITTED | UNKNOWN
|
||||
```
|
||||
|
||||
`UNKNOWN`은 새 key 자동 retry가 아니라 status/reconcile 또는 명시적 사용자 복구로
|
||||
닫는다.
|
||||
|
||||
### 8. Logical deadline, cancel과 retry
|
||||
|
||||
```text
|
||||
LogicalExecutionBudget
|
||||
totalDeadlineMs
|
||||
attemptTimeoutMs
|
||||
maxAttempts
|
||||
maxCumulativeSleepMs
|
||||
maxRetryAfterMs
|
||||
authRecoveryCount = 0 | 1
|
||||
```
|
||||
|
||||
total deadline은 다음 모두를 포함한다.
|
||||
|
||||
- operation/schema lookup과 request encode
|
||||
- credential/CSRF attachment
|
||||
- fetch attempt
|
||||
- body read/decode/schema/mapper
|
||||
- 401 recovery
|
||||
- retry backoff/Retry-After
|
||||
|
||||
각 phase는 남은 total budget보다 긴 timer를 만들지 않는다. timer, abort listener,
|
||||
response reader와 auth waiter는 모든 terminal path에서 정리한다.
|
||||
|
||||
초회, network/status retry와 401 recovery replay를 포함한 **모든 API provider
|
||||
fetch**는 하나의 monotonic `physicalAttemptCount`를 증가시키고 `maxAttempts`를
|
||||
소비한다. `authRecoveryCount`는 추가 상한일 뿐 attempt counter, sleep budget이나
|
||||
total deadline을 reset하거나 우회하지 않는다.
|
||||
|
||||
retry status는 operation의 exact subset만 허용한다.
|
||||
|
||||
```text
|
||||
network failure
|
||||
408
|
||||
429
|
||||
502
|
||||
503
|
||||
504
|
||||
```
|
||||
|
||||
- 429와 503의 `Retry-After`를 injected clock으로 parse한다.
|
||||
- hard ceiling을 넘는 Retry-After는 sleep하지 않고 terminal로 닫는다.
|
||||
- full jitter와 attempt/sleep/elapsed 세 상한을 모두 적용한다.
|
||||
- schema, mapper, 4xx validation/authz/conflict와 redirect failure는 retry하지 않는다.
|
||||
- caller cancel, navigation supersede, runtime teardown, attempt timeout과 logical
|
||||
deadline을 다른 safe failure로 유지한다.
|
||||
- 401 recovery는 auth-required이면서 replay-safe한 operation만 한 번 수행한다.
|
||||
- 동시 401은 session owner의 single-flight recovery를 공유한다.
|
||||
- physical attempt, logical retry와 auth replay를 따로 관측한다.
|
||||
|
||||
### 9. Closed execution
|
||||
|
||||
public executor는 promise rejection 대신 항상 closed result로 끝난다.
|
||||
|
||||
```text
|
||||
RestExecutionResult<T>
|
||||
SUCCESS
|
||||
VALIDATION_REJECTED
|
||||
AUTH_REQUIRED | AUTH_INTEGRATION_FAILURE
|
||||
REQUEST_ABORTED
|
||||
REQUEST_ATTEMPT_TIMEOUT | REQUEST_DEADLINE_EXCEEDED
|
||||
NETWORK_UNREACHABLE
|
||||
RATE_LIMITED | SERVER_FAILURE
|
||||
HTTP_FAILURE
|
||||
CONTENT_TYPE_MISMATCH | BODY_LIMIT_EXCEEDED | MALFORMED_BODY
|
||||
SCHEMA_MISMATCH | MAPPING_CONTRACT_VIOLATION
|
||||
PRECONDITION_FAILED | CONFLICT
|
||||
CONTRACT_INCOMPATIBLE
|
||||
```
|
||||
|
||||
attempt timer와 전체 logical deadline은 error registry, safe user copy와 telemetry
|
||||
bucket에서도 별도 closed kind로 유지한다. 둘 다 raw URL/timing detail을
|
||||
노출하지 않는다.
|
||||
|
||||
operation lookup, URL construction, `Headers`, body serialization, `Request`,
|
||||
auth collaboration, fetch, read, parse, schema와 mapper를 모두 catch/normalize
|
||||
경계 안에 둔다. thrown value/body/header/URL을 failure에 복사하지 않는다.
|
||||
|
||||
### 10. Response admission과 bounded decoder
|
||||
|
||||
body를 `Response.json()`으로 바로 읽지 않는다.
|
||||
|
||||
```text
|
||||
response
|
||||
-> final URL/status/header admission
|
||||
-> exact media type parser
|
||||
-> present/valid Content-Length advisory preflight
|
||||
-> bounded stream reader
|
||||
-> actual browser-visible decoded byte count
|
||||
-> UTF-8/profile decoder
|
||||
-> JSON structural ceiling
|
||||
-> envelope/status codec
|
||||
-> operation response codec
|
||||
-> mapper
|
||||
```
|
||||
|
||||
browser Fetch의 `Response.body`는 일반적으로 content decoding 뒤 stream이므로
|
||||
client가 actual wire/encoded byte를 신뢰성 있게 세었다고 주장하지 않는다.
|
||||
|
||||
- BFF/proxy/CDN가 encoded transfer와 decompression ratio ceiling을 집행한다.
|
||||
- browser client는 present/valid `Content-Length`를 advisory rejection에만 쓰고
|
||||
actual decoded bytes를 hard cap으로 센다.
|
||||
- 표준 `JSON.parse` profile은 decoded-byte cap이 pre-parse resource guard이고
|
||||
depth/node/key/string/item cap은 materialization 뒤 admission guard다.
|
||||
- 구조 cap을 parse 중 강제해야 하는 더 큰 profile은 bounded tokenizing JSON
|
||||
parser를 별도로 선택하고 actual browser evidence를 가져야 한다.
|
||||
|
||||
cap 초과, truncation과 invalid UTF-8에서 reader를 cancel하고 cache에 쓰지 않는다.
|
||||
|
||||
media parser는 type/subtype/parameter를 exact하게 해석한다.
|
||||
|
||||
- `application/json`
|
||||
- approved vendor `application/*+json`
|
||||
- RFC Problem Details profile
|
||||
- explicit 204 no-content
|
||||
|
||||
`includes("application/json")` 검사는 목표 계약이 아니다.
|
||||
|
||||
각 success status는 response codec을 가진다. 현재 envelope는
|
||||
`REST_ENVELOPE_V1` profile로 유지할 수 있지만 모든 REST provider에 강제하지
|
||||
않는다. success status와 error body가 모순되면 status/profile 계약 실패다.
|
||||
|
||||
### 11. Error projection
|
||||
|
||||
backend error는 먼저 status/media별 codec을 통과한다.
|
||||
|
||||
- raw message, stack, body와 arbitrary extensions를 버린다.
|
||||
- code/category는 operation error profile의 allowlist로 mapping한다.
|
||||
- unknown backend code는 closed generic failure다.
|
||||
- validation issue는 최대 count, path/code byte/charset와 allowed path를 제한한다.
|
||||
- request ID, trace ID와 correlation ID도 length/charset cap을 적용한다.
|
||||
- `retryable` backend boolean을 client retry authority로 사용하지 않는다.
|
||||
- 401/403/404 existence-hiding은 provider/product policy를 따른다.
|
||||
- 409 business conflict와 412 representation precondition failure를 분리한다.
|
||||
|
||||
### 12. Cursor pagination
|
||||
|
||||
```text
|
||||
CursorPageWire<T>
|
||||
items
|
||||
nextCursor | null
|
||||
hasMore
|
||||
snapshotToken | null
|
||||
```
|
||||
|
||||
response codec은 item count, item size, cursor/snapshot byte와 total decoded ceiling을
|
||||
검증한다. mapper는 immutable `CursorPage<ApplicationProjection>`을 만든다.
|
||||
|
||||
- cursor는 opaque이며 decode/로그/telemetry 금지
|
||||
- arbitrary next URL을 따라가지 않음
|
||||
- filters/sort/scope/snapshot과 cursor를 exact binding
|
||||
- same cursor 반복, `hasMore=true`인데 cursor 없음, non-progress loop 거절
|
||||
- maximum pages/items/cache bytes 이후 fetch 중지
|
||||
- TanStack infinite query의 root key에는 semantic filter만 넣고 cursor는 bounded
|
||||
`pageParam`으로 관리
|
||||
- offset pagination은 stable small dataset이 증명된 별도 profile만 허용
|
||||
|
||||
current reference list array는 complete pagination 구현이 아니다.
|
||||
|
||||
### 13. Conditional read와 optimistic concurrency
|
||||
|
||||
operation별 owner:
|
||||
|
||||
```text
|
||||
ConditionalProfile
|
||||
NO_STORE
|
||||
APP_ETAG
|
||||
BROWSER_HTTP_CACHE
|
||||
APPLICATION_REVISION
|
||||
```
|
||||
|
||||
모든 operation은 한 profile을 가져야 한다.
|
||||
|
||||
- `NO_STORE`, `APP_ETAG`, `APPLICATION_REVISION`은 Fetch `cache=no-store`.
|
||||
- `BROWSER_HTTP_CACHE`만 exact browser cache mode와 server
|
||||
`Cache-Control`/`Vary` contract를 사용한다.
|
||||
- caller/library default에 맡기는 implicit `NONE`은 없다.
|
||||
|
||||
`APP_ETAG`:
|
||||
|
||||
- strong ETag/generation은 adapter-private metadata
|
||||
- exact operation/query/scope/representation binding과 함께 memory에 보존
|
||||
- `If-None-Match`를 transport가 생성
|
||||
- 304는 same binding의 mapped cached value가 있을 때만 freshness 갱신
|
||||
- 304는 same query-entry cache revision CAS가 성공할 때만 commit
|
||||
- cached value가 없으면 one-time unconditional request 또는 closed failure
|
||||
- app-managed conditional operation은 fetch cache mode를 `no-store`로 고정
|
||||
- full 200 mapped commit과 validator install/update는 같은 entry transaction
|
||||
- query removal/GC, scope/logout, release/contract/schema/mapper epoch reset에서
|
||||
validator sidecar도 함께 폐기
|
||||
- ordinary invalidation에서 validator 보존 여부는 profile이 고정
|
||||
|
||||
`BROWSER_HTTP_CACHE`:
|
||||
|
||||
- standard browser cache가 revalidation을 소유
|
||||
- application이 hidden validator/304 logic을 중복 구현하지 않음
|
||||
- HTTP `Cache-Control`을 TanStack `staleTime`으로 자동 변환하지 않음
|
||||
|
||||
write precondition:
|
||||
|
||||
- exact resource revision을 `If-Match` 또는 body contract로 binding
|
||||
- 412는 `PRECONDITION_FAILED`
|
||||
- current server representation을 refetch한 뒤 feature use case가 overwrite,
|
||||
merge 또는 cancel을 결정
|
||||
- 409 business conflict와 합치지 않음
|
||||
|
||||
ETag/revision은 authorization proof가 아니며 raw value를 diagnostics에 넣지 않는다.
|
||||
|
||||
### 14. Schema와 Mapper 연결
|
||||
|
||||
VD-24의 typed codec/mapper를 사용한다.
|
||||
|
||||
```text
|
||||
unknown
|
||||
-> RuntimeCodec<ValidatedResponseDto>
|
||||
-> Mapper<ValidatedResponseDto, ApplicationProjection>
|
||||
-> Result
|
||||
```
|
||||
|
||||
request는 strict/normalized parsed output만 serialize한다. ordinary additive
|
||||
response는 required/discriminant를 검증하고 unknown field를 폐기한다. control,
|
||||
authorization와 sealed union은 unknown을 거절한다.
|
||||
|
||||
generated OpenAPI client/DTO를 선택해도 adapter-private이다. handwritten gateway와
|
||||
mapper를 제거하지 않는다.
|
||||
|
||||
### 15. Query cache와의 관계
|
||||
|
||||
REST transport는 raw response cache를 소유하지 않는다. VD-25가 mapped application
|
||||
projection을 TanStack Query에 admission한다.
|
||||
|
||||
- operation registry의 cache profile만 query를 만들 수 있다.
|
||||
- Query retry는 `false`; REST transport가 network retry를 소유한다.
|
||||
- URL, ETag, idempotency key, Response와 DTO를 query key/value에 넣지 않는다.
|
||||
- mutation success 뒤 registered invalidation topic 또는 exact typed seed policy만
|
||||
사용한다.
|
||||
- REST timeout/error를 Query가 다시 network retry하지 않는다.
|
||||
|
||||
### 16. Contract source와 release coherence
|
||||
|
||||
OpenAPI가 backend authority인 제품:
|
||||
|
||||
```text
|
||||
authenticated immutable OpenAPI artifact
|
||||
-> source digest/provenance
|
||||
-> lint + breaking diff
|
||||
-> pinned deterministic generation
|
||||
-> runtime codec parity
|
||||
-> adapter-private DTO/client
|
||||
-> handwritten mapper/gateway
|
||||
```
|
||||
|
||||
CI:
|
||||
|
||||
- generator/runtime/plugin/Node version pin
|
||||
- clean checkout regenerate diff 0
|
||||
- stable operation ID
|
||||
- source/artifact/generated output digest
|
||||
- runtime codec vs specification fixtures
|
||||
- N/N-1 and future-major failure
|
||||
- generated import boundary
|
||||
|
||||
global `API_CONTRACT_VERSION`은 selected contract-set compatibility와 digest에
|
||||
연결한다. runtime config와 release manifest의 같은 문자열만으로 backend
|
||||
compatibility를 주장하지 않는다.
|
||||
|
||||
major version 전략은 URI version 또는 vendor media version 중 provider가 하나를
|
||||
선택한다. 동시에 둘을 임의 증가시키지 않는다. v1/v2 adapters는 같은
|
||||
application gateway를 구현할 수 있지만 같은 query entry에 representation을
|
||||
섞지 않는다.
|
||||
|
||||
### 17. Security와 observability
|
||||
|
||||
관측 허용:
|
||||
|
||||
- semantic operation/provider/profile ID
|
||||
- method/semantics
|
||||
- outcome/AppFailure kind/HTTP status group
|
||||
- logical retry, auth recovery와 physical attempt bucket
|
||||
- duration/deadline/request-response byte/item bucket
|
||||
- conditional/cache outcome
|
||||
|
||||
금지:
|
||||
|
||||
- URL, path/search/header/body
|
||||
- cursor/snapshot/ETag/revision
|
||||
- idempotency/CSRF/auth token
|
||||
- raw backend code/message/request/trace value
|
||||
- application resource/account/tenant ID
|
||||
|
||||
client correlation ID는 bounded syntax로 request에 전달하고 server-projected
|
||||
request/trace ID는 safe failure/observation에만 제한한다. 한 logical execution은
|
||||
terminal event 하나를 만든다.
|
||||
|
||||
### 18. Composition과 readiness
|
||||
|
||||
REST v2 composition owner가 다음을 atomic하게 만든다.
|
||||
|
||||
```text
|
||||
parse provider/config
|
||||
-> compose collision-free operation registry
|
||||
-> resolve codec/mapper/policy references
|
||||
-> install auth/CSRF owner
|
||||
-> run compatibility/provider probes
|
||||
-> publish READY facade
|
||||
```
|
||||
|
||||
partial registry/client를 application에 노출하지 않는다.
|
||||
|
||||
```text
|
||||
Selection
|
||||
TrafficAdmission
|
||||
RuntimeHealth
|
||||
PromotionEvidence
|
||||
```
|
||||
|
||||
primary status가 `COMPOSED`여도 provider/auth/CSRF/idempotency/conditional
|
||||
conformance가 없으면 `TrafficAdmission=DISABLED`와
|
||||
`PromotionEvidence=MISSING | PARTIAL | EXPIRED`로 닫는다.
|
||||
|
||||
kill switch:
|
||||
|
||||
- provider 전체
|
||||
- operation family
|
||||
- keyed retry/401 replay
|
||||
- conditional request
|
||||
- optimistic mutation
|
||||
- query cache admission
|
||||
|
||||
### 19. Test와 provider conformance
|
||||
|
||||
deterministic:
|
||||
|
||||
- contribution collision, missing codec/mapper/profile와 owner mismatch
|
||||
- URI template/path/query canonicalization/base-prefix preservation
|
||||
- method/body/replay/status/media coherence
|
||||
- anonymous/cookie/bearer credentials와 Fetch cache mode matrix
|
||||
- auth final-request mutation 공격과 unavailable auth fetch 0
|
||||
- attempt timeout vs total deadline, timer/listener/reader cleanup
|
||||
- concurrent 401 single-flight
|
||||
- 401 replay를 포함한 monotonic physical-attempt cap
|
||||
- retry matrix, injected-clock 429/503 Retry-After
|
||||
- same key/same payload replay와 same key/different payload rejection
|
||||
- 204/304/412/422/problem/envelope
|
||||
- oversized/truncated/malformed/decompression overflow
|
||||
- cursor loop/snapshot/page ceiling
|
||||
- ETag 304 without cache, If-Match 412
|
||||
- mapper failure와 redaction
|
||||
|
||||
actual staging provider:
|
||||
|
||||
- HTTPS/base path/CORS/preflight/credential
|
||||
- cookie/Origin/CSRF
|
||||
- idempotency concurrent claim/TTL/reconcile
|
||||
- status/media/error codec
|
||||
- cursor/snapshot/conditional semantics
|
||||
- rate limit/Retry-After
|
||||
- correlation/request/trace projection
|
||||
- proxy/CDN content encoding and body cap
|
||||
- browser decoded-byte cap과 provider encoded/decompression ceiling
|
||||
- outbound correlation, success status group와 401 physical-attempt observation
|
||||
|
||||
MSW 통과는 provider conformance가 아니다.
|
||||
|
||||
### 20. Rollout
|
||||
|
||||
1. collision-aware v2 registry/codec과 boot-time binding 검증을 설치한다.
|
||||
2. auth fail-closed, final invariant, bounded decoder와 total deadline을 local
|
||||
reference vertical에서 검증한다.
|
||||
3. actual provider에 같은 fixture를 실행하고 read operation을 canary한다.
|
||||
4. keyed command는 backend idempotency conformance 뒤 별도 canary한다.
|
||||
5. pagination/conditional operation을 각각 별도 traffic gate로 올린다.
|
||||
6. provider/browser/operations evidence가 complete인 operation만 enabled한다.
|
||||
7. rollback은 우선 safe unavailable로 내리고 contract artifact/frontend/backend를
|
||||
coherent set으로 복구한다. v1 fallback은 해당 operation의 unexpired
|
||||
provider/security evidence가 있고 incident가 v1/shared boundary에 영향이 없으며
|
||||
auth fail-close/final invariant hardening이 유지될 때만 허용한다.
|
||||
|
||||
### 21. Removal
|
||||
|
||||
GraphQL/Connect/gRPC-Web/REST Gateway 선택을 취소해도 REST v2 common execution
|
||||
context는 남을 수 있다.
|
||||
REST provider 제거 시:
|
||||
|
||||
1. 신규 operation admission 중지
|
||||
2. read cancel, command effect certainty reconcile
|
||||
3. auth/CSRF/retry timer와 response reader close
|
||||
4. current scope query cache clear/invalidate
|
||||
5. operation/schema/mapper/query profile 제거
|
||||
6. provider config/proxy/dependency/fixture 제거
|
||||
7. production module inventory와 backend route retirement evidence
|
||||
|
||||
## 완료 기준
|
||||
|
||||
- installed REST operation이 typed path/search/body/success/error codec과 mapper에
|
||||
하나의 definition으로 연결된다.
|
||||
- auth unavailable 또는 mutated final request에서 fetch가 0회다.
|
||||
- 모든 throw/response size/status/media/schema/mapper failure가 closed result다.
|
||||
- total logical deadline이 auth/recovery/backoff/decode/mapper를 포함한다.
|
||||
- replay는 declared policy와 actual backend idempotency evidence를 가진다.
|
||||
- complete cursor page와 conditional/412 state가 bounded하게 동작한다.
|
||||
- response DTO/URL/header/token/validator가 application/query/log에 없다.
|
||||
- actual provider conformance와 rollback/removal drill이 통과한다.
|
||||
@@ -0,0 +1,585 @@
|
||||
# VD-24: Runtime Schema와 boundary Mapper
|
||||
|
||||
- 상태: Accepted — reference typed codec/mapper baseline composed, artifact governance pending
|
||||
- 결정일: 2026-07-28
|
||||
- reference REST Schema/Mapper vertical: `COMPOSED`
|
||||
- semantic compatibility/codegen governance delta:
|
||||
`DESIGNED_NOT_IMPLEMENTED`
|
||||
- generated API product selection: `NOT_SELECTED`
|
||||
- 관련 결정: VD-13, VD-23, VD-25, VD-26, VD-27, VD-29, VD-30
|
||||
- 상세 설계:
|
||||
[API contract, Schema, Mapper와 Server State](../api-contract-schema-mapper-and-server-state.md)
|
||||
|
||||
## 배경
|
||||
|
||||
현재 reference vertical은 다음 trust path를 실제 실행한다.
|
||||
|
||||
```text
|
||||
HTTP response
|
||||
-> common envelope Zod
|
||||
-> feature payload Zod
|
||||
-> feature mapper
|
||||
-> domain factory
|
||||
-> application view
|
||||
-> TanStack Query
|
||||
```
|
||||
|
||||
reference baseline은 schema contribution collision을 boot 전에 거절하고,
|
||||
schema version/direction/unknown-field policy를 기록한다. request는 strict reject,
|
||||
ordinary response DTO는 strip projection을 사용하며 list item 수와 response/cache
|
||||
byte admission을 제한한다. mapper는 no-throw `MappingResult`를 반환하고 예상 가능한
|
||||
drift는 `MAPPING_CONTRACT_VIOLATION`으로 분류한다.
|
||||
|
||||
runtime schema codec과 mapper contribution은 collision-aware composer로 설치되고,
|
||||
각 REST operation의 path/request/response schema와 mapper input schema reference를
|
||||
boot 전에 exact resolve한다. operation별 cast-free result guard도 raw executor의
|
||||
성공 값을 fail-closed로 재검증한다. 남은 delta는
|
||||
actual codec fingerprint/source provenance/generated artifact join과 전체 scalar
|
||||
policy set이다.
|
||||
|
||||
이 결정은 runtime validation을 특정 library 이름으로 축소하지 않는다. 현재
|
||||
owned schema는 Zod를 사용하지만 GraphQL generated types와 protobuf messages에도
|
||||
동일한 trust transition을 적용한다.
|
||||
|
||||
## 결정
|
||||
|
||||
### 1. 다섯 validation 경계를 분리한다
|
||||
|
||||
| 경계 | owner | 목적 |
|
||||
| --- | --- | --- |
|
||||
| route/form input | presentation/feature | 사용자 입력 정규화와 UX issue |
|
||||
| application command/query input | application/feature | use-case precondition과 canonical semantic input |
|
||||
| transport request wire | adapter/contract | exact outbound representation |
|
||||
| transport response wire | adapter/contract | untrusted server bytes/message 검증 |
|
||||
| domain invariant | domain | 업무상 유효한 entity/value 생성 |
|
||||
|
||||
하나의 Zod schema를 form, API request, response와 domain에 재사용하지 않는다.
|
||||
field 이름이 같아도 trust source와 failure semantics가 다르다.
|
||||
|
||||
### 2. TypeScript와 generated type은 proof가 아니다
|
||||
|
||||
```text
|
||||
unknown bytes/message
|
||||
-> bounded decoder
|
||||
-> RuntimeCodec<ValidatedDto>
|
||||
-> ValidatedDto
|
||||
-> BoundaryMapper<ValidatedDto, ApplicationValue>
|
||||
-> MappingResult<ApplicationValue>
|
||||
```
|
||||
|
||||
`as Dto`, generic `execute<T>()`, generated TypeScript interface와 protobuf class
|
||||
instance는 runtime proof를 만들지 않는다.
|
||||
|
||||
목표 API:
|
||||
|
||||
```text
|
||||
RuntimeCodec<Input, Output>
|
||||
schemaId
|
||||
parse(input, budget) -> ValidationResult<Output>
|
||||
|
||||
BoundaryMapper<ValidatedDto, ApplicationValue>
|
||||
mapperId
|
||||
inputSchemaId
|
||||
map(dto) -> MappingResult<ApplicationValue>
|
||||
|
||||
BoundOperation<Input, Dto, Value>
|
||||
requestCodec
|
||||
responseCodec
|
||||
mapper
|
||||
```
|
||||
|
||||
operation definition 생성 시 codec output과 mapper input type을 compiler가
|
||||
연결한다. runtime registry도 same IDs/fingerprints를 검증한다.
|
||||
|
||||
### 3. Schema registry v2
|
||||
|
||||
```text
|
||||
SchemaDefinitionV2
|
||||
schemaId
|
||||
wireVersion
|
||||
boundary
|
||||
protocol
|
||||
sourceKind = OWNED | GENERATED
|
||||
sourceArtifactId
|
||||
sourceArtifactDigest
|
||||
codecId
|
||||
codecFingerprint
|
||||
unknownFieldPolicy
|
||||
numericPolicyId
|
||||
temporalPolicyId
|
||||
providerMaxEncodedBytes
|
||||
maxDecodedBytes
|
||||
maxDepth
|
||||
maxNodes
|
||||
maxObjectKeys
|
||||
maxStringBytes
|
||||
maxCollectionItems
|
||||
compatibilityPolicy
|
||||
dataClassification
|
||||
owner
|
||||
```
|
||||
|
||||
정적 metadata는 실행 codec definition에서 결정적으로 투영한다. 실제 codec
|
||||
resolver가 없는 schema ID, fingerprint가 다른 resolver와 duplicate ID는
|
||||
contribution composition에서 실패한다.
|
||||
|
||||
`codecFingerprint`는 library 내부 AST serialization을 무조건 신뢰하지 않는다.
|
||||
프로젝트가 소유한 canonical schema manifest를 사용한다.
|
||||
|
||||
```text
|
||||
canonical schema manifest
|
||||
field/path
|
||||
required/nullability
|
||||
scalar/format/range
|
||||
enum/discriminant
|
||||
collection/item ceiling
|
||||
unknown-field policy
|
||||
transform identifier/version
|
||||
```
|
||||
|
||||
Zod upgrade로 내부 representation이 바뀌어도 canonical meaning diff가 안정적이어야
|
||||
한다.
|
||||
|
||||
### 4. Decode budget
|
||||
|
||||
Content-Length나 protobuf frame length만으로 충분하지 않다.
|
||||
|
||||
```text
|
||||
ValidationBudget
|
||||
decodedBytesRemaining
|
||||
nodesRemaining
|
||||
depthRemaining
|
||||
objectKeysRemaining
|
||||
stringBytesRemaining
|
||||
collectionItemsRemaining
|
||||
deadlineRemaining
|
||||
```
|
||||
|
||||
- BFF/proxy/provider가 actual wire/encoded transfer와 decompression-ratio cap을
|
||||
집행한다.
|
||||
- browser Fetch adapter는 present/valid Content-Length를 advisory preflight로만
|
||||
사용하고 browser-visible decoded stream bytes를 hard cap으로 센다.
|
||||
- codec은 depth/node/key/string/item cap을 적용한다.
|
||||
- collection nested item도 global budget을 함께 소모한다.
|
||||
- transform/refine도 남은 logical deadline 안에서 동기적이고 bounded해야 한다.
|
||||
- async network/storage refinement를 runtime wire schema에 넣지 않는다.
|
||||
- budget 초과는 validation issue list를 무한 생성하지 않고 첫 bounded summary로
|
||||
닫는다.
|
||||
|
||||
표준 `JSON.parse` profile에서 decoded-byte cap은 pre-parse guard지만
|
||||
depth/node/key/string/item cap은 materialization 뒤 admission guard다. parse 중
|
||||
구조 cap이 필요한 payload는 bounded tokenizing parser를 별도 profile로 선택하며,
|
||||
그 구현 전에는 큰 byte ceiling을 승인하지 않는다.
|
||||
|
||||
current request `limit <= 100`은 response item ceiling이 아니다. response schema가
|
||||
items maximum과 total byte budget을 별도로 검증한다.
|
||||
|
||||
### 5. Unknown-field 정책
|
||||
|
||||
```text
|
||||
UnknownFieldPolicy
|
||||
REJECT_UNKNOWN
|
||||
STRIP_UNKNOWN
|
||||
```
|
||||
|
||||
`PRESERVE_UNKNOWN`은 application boundary에서 허용하지 않는다.
|
||||
|
||||
| schema class | 기본 정책 |
|
||||
| --- | --- |
|
||||
| request, config, command, capability | `REJECT_UNKNOWN` |
|
||||
| auth/authorization/control envelope | `REJECT_UNKNOWN` |
|
||||
| ordinary additive REST response DTO | `STRIP_UNKNOWN` |
|
||||
| GraphQL selected data object | requested field shape만 투영 |
|
||||
| sealed discriminated union | unknown discriminator 거절 |
|
||||
| protobuf generated message | codec/library unknown-field behavior 뒤 mapper는 known projection만 사용 |
|
||||
|
||||
current response `.strict()`를 모두 `.passthrough()`로 바꾸지 않는다. unknown
|
||||
field를 제거한 typed projection만 mapper로 보낸다. unknown field 이름/value를
|
||||
log에 남기지 않는다. 필요한 경우 low-cardinality `unknown-field-detected`
|
||||
observation만 sampling한다.
|
||||
|
||||
### 6. Request와 response 방향성
|
||||
|
||||
request:
|
||||
|
||||
- strict field set
|
||||
- trim/coerce/default/normalization 정책이 명시됨
|
||||
- parsed output만 transport가 serialize
|
||||
- input 원본을 query key나 request에 따로 사용하지 않음
|
||||
- route/search/application command 변환이 같은 canonical semantic input을 공유
|
||||
|
||||
response:
|
||||
|
||||
- untrusted value를 coerce하지 않음
|
||||
- required/nullability/discriminant/range를 검증
|
||||
- additive unknown은 profile에 따라 strip
|
||||
- default value를 서버가 보낸 값처럼 조용히 생성하지 않음
|
||||
- missing/null/empty를 mapper가 명시적으로 소진
|
||||
|
||||
`z.coerce`는 URL/form 같은 string input 경계에서만 허용한다. JSON/protobuf
|
||||
response에 적용하지 않는다.
|
||||
|
||||
### 7. Scalar 의미
|
||||
|
||||
#### ID
|
||||
|
||||
- opaque bounded string
|
||||
- empty/control/overlong 거절
|
||||
- 업무 계약이 없는 case folding, Unicode normalization과 numeric parse 금지
|
||||
- account/resource ID를 diagnostics label이나 physical cache key에 직접 넣지 않음
|
||||
|
||||
#### Integer와 decimal
|
||||
|
||||
- JSON integer는 finite safe integer 범위를 증명
|
||||
- `int64`/`uint64`는 JavaScript number로 mapping하지 않음
|
||||
- protobuf bigint/string representation은 adapter-private
|
||||
- money/decimal/high precision은 canonical decimal string + currency/scale policy
|
||||
- `NaN`, Infinity와 negative zero가 의미상 허용되는지 explicit
|
||||
- string-to-number response coercion 금지
|
||||
|
||||
#### Time
|
||||
|
||||
- exact RFC 3339 profile과 offset/precision을 검증
|
||||
- date-only, instant, local date-time과 duration을 다른 type으로 둠
|
||||
- leap/invalid date를 JavaScript `Date` normalization에 맡기지 않음
|
||||
- protobuf Timestamp/Duration range/nanos를 검증
|
||||
- mapper가 application temporal value로 변환
|
||||
- locale/timezone formatting은 presentation에서만 수행
|
||||
|
||||
#### Null과 absent
|
||||
|
||||
```text
|
||||
ABSENT
|
||||
NULL
|
||||
EMPTY
|
||||
VALUE
|
||||
```
|
||||
|
||||
네 의미를 schema/mapper contract에 명시한다. current mapper처럼 “string이
|
||||
아니면 모두 null”로 합치지 않는다. optional server field의 default가 필요하면
|
||||
application policy가 이름 있는 결정으로 적용한다.
|
||||
|
||||
#### Enum/union/oneof
|
||||
|
||||
- unknown discriminator는 sealed control union에서 fail-closed
|
||||
- evolvable business enum은 domain이 explicit `UNKNOWN` case와 UX를 소유한
|
||||
경우에만 mapping
|
||||
- raw unknown string/number를 domain에 전달하지 않음
|
||||
- protobuf enum zero value, unknown numeric enum과 oneof absence를 명시적으로
|
||||
처리
|
||||
|
||||
#### Binary
|
||||
|
||||
- REST base64는 decoded byte cap과 canonical encoding profile 필요
|
||||
- GraphQL upload/binary는 이 JSON schema 경계의 기본 기능이 아님
|
||||
- protobuf `bytes`는 bounded copy/stream policy 뒤에만 application으로 projection
|
||||
- large binary는 File/transfer capability를 사용
|
||||
|
||||
### 8. Transport-specific schema
|
||||
|
||||
#### REST
|
||||
|
||||
- exact status/media/envelope profile 뒤 operation DTO codec 실행
|
||||
- error body도 별도 bounded codec
|
||||
- Problem Details의 type/title/detail/instance를 raw UI copy로 사용하지 않음
|
||||
- response envelope와 payload unknown policy를 따로 설정
|
||||
|
||||
#### GraphQL
|
||||
|
||||
- variables와 selected `data` shape에 separate codec
|
||||
- top-level `data`, `errors`, `extensions`를 GraphQL response codec이 검증
|
||||
- errors path/message/extensions는 safe failure mapper 전 untrusted
|
||||
- partial policy가 허용한 missing/null만 operation DTO type에 표현
|
||||
- persisted operation manifest의 schema digest와 codec fingerprint 일치
|
||||
|
||||
#### gRPC-Web
|
||||
|
||||
- frame/trailer 검증 뒤 generated protobuf decoder 실행
|
||||
- generated decode success 뒤에도 semantic validator가 range/presence/enum/oneof를
|
||||
검증
|
||||
- descriptor digest/message full name과 codec binding 일치
|
||||
- `google.rpc.Status` details는 allowlisted type만 decode
|
||||
|
||||
#### Connect-Web/Connect
|
||||
|
||||
- Connect unary HTTP/error 또는 stream EndStream proof 뒤 generated message decode
|
||||
- JSON/binary encoding과 descriptor/message binding을 operation profile에 고정
|
||||
- generated decode와 `ConnectError` code는 semantic domain proof가 아니므로
|
||||
같은 validator/mapper와 safe failure vocabulary를 통과
|
||||
|
||||
#### Protobuf REST Gateway
|
||||
|
||||
- HttpRule/ProtoJSON/status/error profile 뒤 ordinary REST DTO codec 실행
|
||||
- generated OpenAPI type이나 ProtoJSON message를 application model로 사용하지 않음
|
||||
- direct gateway와 curated BFF의 envelope/schema를 같은 codec으로 추측하지 않음
|
||||
|
||||
### 9. Boundary Mapper v2
|
||||
|
||||
```text
|
||||
MapperDefinitionV2
|
||||
mapperId
|
||||
mapperVersion
|
||||
inputSchemaId
|
||||
outputContractId
|
||||
scalarPolicySetId
|
||||
maxOutputItems
|
||||
maxEstimatedOutputBytes
|
||||
owner
|
||||
```
|
||||
|
||||
mapper는:
|
||||
|
||||
- pure
|
||||
- deterministic
|
||||
- synchronous
|
||||
- side-effect-free
|
||||
- locale/timezone-independent
|
||||
- input mutation 없음
|
||||
- immutable output
|
||||
- exhaustive
|
||||
- bounded
|
||||
|
||||
mapper가 호출하면 안 되는 것:
|
||||
|
||||
- fetch, generated client, QueryClient
|
||||
- clock/random
|
||||
- storage/cache
|
||||
- telemetry/logger
|
||||
- DOM/browser API
|
||||
- authorization/feature flag
|
||||
|
||||
### 10. Mapping result
|
||||
|
||||
```text
|
||||
MappingResult<T>
|
||||
{ ok: true, value: T }
|
||||
{ ok: false,
|
||||
error:
|
||||
MAPPING_INVARIANT_REJECTED |
|
||||
UNSUPPORTED_WIRE_VALUE |
|
||||
OUTPUT_LIMIT_EXCEEDED }
|
||||
```
|
||||
|
||||
예상 가능한 domain invariant/unknown enum/temporal conversion 실패는 throw하지
|
||||
않는다. programming defect가 throw되더라도 adapter boundary가
|
||||
`MAPPING_CONTRACT_VIOLATION`으로 정규화한다. raw DTO/value/path/message를 failure에
|
||||
복사하지 않는다.
|
||||
|
||||
`UNKNOWN_FAILURE`는 mapper drift의 정상 분류가 아니다. operation ID,
|
||||
schema/mapper profile/version과 safe outcome만 관측한다.
|
||||
|
||||
### 11. Domain, application projection과 view
|
||||
|
||||
```text
|
||||
ValidatedDto
|
||||
-> domain value/entity factory
|
||||
-> ApplicationReadModel / command result
|
||||
-> presentation-only ViewModel
|
||||
```
|
||||
|
||||
- DTO는 adapter/contracts 내부
|
||||
- domain은 transport nullability/error/envelope를 모름
|
||||
- application read model은 query cache에 넣을 수 있는 immutable plain value
|
||||
- presentation view는 locale/formatted copy와 UI-only optimistic marker를 소유
|
||||
- domain class/service, function, native object와 generated message를 Query cache에
|
||||
넣지 않음
|
||||
|
||||
current reference가 domain을 거쳐 view를 만드는 구조는 유지한다. 단 collection과
|
||||
return object를 immutable/bounded하게 만들고 mapping type proof를 연결한다.
|
||||
|
||||
### 12. Collection mapping
|
||||
|
||||
- input array/page count는 codec에서 먼저 제한
|
||||
- mapper는 output count와 estimated bytes를 다시 제한
|
||||
- item 하나 실패 시 partial collection을 success/cache하지 않음
|
||||
- stable identity, ordering, duplicate 의미는 feature contract가 결정
|
||||
- duplicate ID를 임의로 마지막 값으로 덮지 않음
|
||||
- mapper가 sort/filter/deduplicate를 한다면 이름 있는 policy와 fixture 필요
|
||||
- pagination page/snapshot binding을 보존
|
||||
|
||||
estimated output bytes는 quota/serialization exact value가 아니라 cache admission
|
||||
ceiling용 보수적 측정이다. 측정 실패는 unlimited로 간주하지 않고 cache
|
||||
admission을 거절한다.
|
||||
|
||||
### 13. Generated와 owned source
|
||||
|
||||
```text
|
||||
backend-authoritative contract
|
||||
-> authenticated immutable source artifact
|
||||
-> source digest + provenance
|
||||
-> pinned codegen
|
||||
-> adapter-private DTO/client/codec
|
||||
-> owned semantic validator where required
|
||||
-> handwritten boundary mapper
|
||||
```
|
||||
|
||||
| protocol | generated source 후보 | 반드시 owned인 것 |
|
||||
| --- | --- | --- |
|
||||
| REST | OpenAPI DTO/client/codec | gateway, mapper, application model, query policy |
|
||||
| GraphQL | schema types, operation types | persisted manifest policy, runtime result/error codec, mapper |
|
||||
| gRPC-Web | protobuf messages/client | semantic validation, failure mapping, mapper, stream reducer |
|
||||
|
||||
normal build가 네트워크에서 최신 schema를 암묵적으로 내려받지 않는다. source
|
||||
fetch/update는 authenticated explicit workflow이며 reviewable diff를 만든다.
|
||||
|
||||
generator:
|
||||
|
||||
- exact package/plugin/runtime/Node version pin
|
||||
- reproducible output
|
||||
- generated directory 수동 수정 금지
|
||||
- clean regenerate diff 0
|
||||
- license/SBOM/secret scan
|
||||
- vendor import boundary
|
||||
- generated artifact removal gate
|
||||
|
||||
현재 `generated-api` recipe의 generic `execute<TOutput>(unknown)`와 caller-selected
|
||||
cast는 production schema proof가 아니다.
|
||||
|
||||
### 14. Compatibility
|
||||
|
||||
change classification:
|
||||
|
||||
| 변경 | 기본 판정 |
|
||||
| --- | --- |
|
||||
| optional ordinary response field 추가 + strip policy | additive |
|
||||
| request required field 추가 | breaking |
|
||||
| response required field 제거/rename/type/nullability 축소 | breaking |
|
||||
| enum value 추가 | domain unknown policy에 따라 additive 또는 breaking |
|
||||
| numeric range/precision/temporal profile 변경 | semantic breaking |
|
||||
| mapper output meaning/identity/order 변경 | application breaking |
|
||||
| unknown-field policy 변경 | compatibility review |
|
||||
| codec transform/default 변경 | semantic diff 필수 |
|
||||
|
||||
`apiContractVersion` 하나만 올리지 않는다.
|
||||
|
||||
```text
|
||||
ContractSetManifest
|
||||
globalCompatibilityVersion
|
||||
REST/OpenAPI artifact digest
|
||||
GraphQL schema + persisted operation digest
|
||||
protobuf descriptor digest
|
||||
runtime schema registry digest
|
||||
mapper registry digest
|
||||
query policy digest
|
||||
```
|
||||
|
||||
- N과 N-1 fixture를 보존
|
||||
- breaking deployment는 old frontend window와 backend compatibility를 고려
|
||||
- future major는 fail-closed
|
||||
- rollback은 frontend, generated artifacts, config, BFF/router/proxy와 backend
|
||||
compatibility를 coherent set으로 복구
|
||||
- mapper-only semantic change도 cache/release epoch invalidation을 검토
|
||||
|
||||
### 15. Failure와 cache admission
|
||||
|
||||
다음 상태에서는 cache write가 0회다.
|
||||
|
||||
- body/frame limit
|
||||
- envelope/status/media mismatch
|
||||
- operation schema mismatch
|
||||
- mapper failure
|
||||
- scope/runtime generation mismatch
|
||||
- output item/byte ceiling 초과
|
||||
- incompatible contract/source digest
|
||||
|
||||
stale data를 유지할지는 VD-25 query profile이 결정한다. schema/mapper
|
||||
incompatibility를 ordinary transient network failure와 동일하게 retry하지 않는다.
|
||||
|
||||
### 16. Security와 privacy
|
||||
|
||||
- validation failure에 raw value를 포함하지 않음
|
||||
- issue path/code를 allowlist와 count/byte cap으로 projection
|
||||
- schema/mapper error가 PII field/value를 diagnostics에 넣지 않음
|
||||
- prototype pollution key와 accessor/class/native object 거절
|
||||
- `structuredClone` 성공을 safe plain-data proof로 사용하지 않음
|
||||
- generated code가 arbitrary URL/header/logger를 application에 노출하지 않음
|
||||
- source artifact와 generator provenance 검증
|
||||
- schema가 frontend authorization boundary라는 주장 금지
|
||||
|
||||
관측 허용:
|
||||
|
||||
- operation/schema/mapper ID와 version
|
||||
- source/compatibility outcome
|
||||
- validation/mapping failure kind
|
||||
- encoded/decoded/output size와 item bucket
|
||||
- unknown-field detected bucket
|
||||
|
||||
source digest 실제 값, field path/value, DTO, GraphQL error path와 protobuf payload는
|
||||
high-cardinality/sensitive이므로 telemetry label에 넣지 않는다.
|
||||
|
||||
### 17. Testing
|
||||
|
||||
schema:
|
||||
|
||||
- missing/null/empty/unknown field
|
||||
- numeric safe bounds, decimal, negative zero, NaN/Infinity
|
||||
- RFC 3339/Timestamp/Duration edge
|
||||
- enum/union/oneof future value
|
||||
- depth/node/key/string/array/byte cap
|
||||
- invalid UTF-8, base64와 binary cap
|
||||
- N/N-1/future-major
|
||||
|
||||
registry:
|
||||
|
||||
- duplicate ID before spread
|
||||
- missing/mismatched codec/mapper resolver
|
||||
- codec fingerprint/source digest drift
|
||||
- operation schema/mapper output type binding
|
||||
- orphan/owner/version mismatch
|
||||
|
||||
mapper:
|
||||
|
||||
- typed DTO only
|
||||
- deterministic/pure/input unmodified
|
||||
- immutable output
|
||||
- no throw for expected semantic rejection
|
||||
- collection partial failure
|
||||
- item/output byte ceiling
|
||||
- missing/null/date/numeric/unknown enum matrix
|
||||
- no raw value leakage
|
||||
|
||||
codegen:
|
||||
|
||||
- source provenance/digest
|
||||
- lint/breaking
|
||||
- clean reproducible generation
|
||||
- generated import boundary
|
||||
- runtime codec fixture parity
|
||||
- dependency/removal inventory
|
||||
|
||||
integration:
|
||||
|
||||
- bytes → decoder → schema → mapper → application/query
|
||||
- schema/mapper drift에서 cache write 0
|
||||
- old runtime generation result 폐기
|
||||
- actual REST/GraphQL/gRPC provider fixture
|
||||
|
||||
### 18. Rollout
|
||||
|
||||
1. current string-dispatch schema/mapper를 그대로 두고 typed definition builder를
|
||||
추가한다.
|
||||
2. reference operation에 shadow validation/mapping을 실행하되 secondary result를
|
||||
UI/cache에 쓰지 않는다.
|
||||
3. collision-aware contribution composer와 codec fingerprint를 먼저 blocking한다.
|
||||
4. bounded decoder/collection ceiling을 query read부터 canary한다.
|
||||
5. typed mapping result와 failure taxonomy를 적용한다.
|
||||
6. current unchecked binder/cast를 제거한다.
|
||||
7. OpenAPI/GraphQL/proto generation은 제품 contract source가 선택된 것만
|
||||
별도 canary한다.
|
||||
8. schema/mapper version을 release/cache epoch와 연결한다.
|
||||
|
||||
rollback은 codec schema number를 낮추거나 cache의 incompatible value를 억지로
|
||||
decode하지 않는다. old adapter/backend path와 coherent artifact로 돌리고 current
|
||||
scope의 incompatible mapped cache를 폐기한다.
|
||||
|
||||
## 완료 기준
|
||||
|
||||
- runtime codec output과 mapper input이 compiler/runtime registry 양쪽에서 연결된다.
|
||||
- schema registry가 actual meaning fingerprint, provenance와 budget을 가진다.
|
||||
- duplicate contribution이 overwrite 전에 실패한다.
|
||||
- request strict/response additive 방향 정책이 test로 증명된다.
|
||||
- scalar/null/enum/collection 의미가 mapper policy로 닫힌다.
|
||||
- mapper가 typed DTO만 받고 expected failure를 `Result`로 반환한다.
|
||||
- generated DTO/message가 domain/application/presentation/query public type에 없다.
|
||||
- schema/mapper failure에서 cache write와 raw-data observation이 0회다.
|
||||
- N/N-1, breaking diff, provider fixture와 rollback/removal drill이 통과한다.
|
||||
@@ -0,0 +1,802 @@
|
||||
# VD-25: Server State Cache lifecycle
|
||||
|
||||
- 상태: Accepted — reference bound-query/input-aware mutation baseline composed, lifecycle delta pending
|
||||
- 결정일: 2026-07-28
|
||||
- TanStack Query memory runtime: `COMPOSED`
|
||||
- reference bound-query/profile/input-aware duplicate coordination: `COMPOSED`
|
||||
- session-generation/identity lifecycle: `COMPOSED`
|
||||
- conditional sidecar/optimistic layer/cursor runtime: `AVAILABLE_NOT_COMPOSED`
|
||||
- account projection/infinite-query/effect reconciliation delta:
|
||||
`DESIGNED_NOT_IMPLEMENTED`
|
||||
- normalized graph cache: `NOT_SELECTED`
|
||||
- query persistence product selection: `NOT_SELECTED`
|
||||
- 관련 결정: VD-13, VD-23, VD-24, VD-26, VD-27, VD-29, VD-30
|
||||
- 상세 설계:
|
||||
[API contract, Schema, Mapper와 Server State](../api-contract-schema-mapper-and-server-state.md)
|
||||
|
||||
## 배경
|
||||
|
||||
현재 production bootstrap은 runtime별 QueryClient와 invalidation coordinator를
|
||||
실제로 조립한다. reference presentation은 application input을
|
||||
`useApplicationQuery()`/`useApplicationMutation()`에 연결하며 다음을 제공한다.
|
||||
|
||||
- AbortSignal cancellation
|
||||
- finite global stale/gc default
|
||||
- Query network retry off
|
||||
- stale-degraded 표시
|
||||
- exact-key optimistic snapshot/rollback
|
||||
- conflict 표면
|
||||
- mutation topic lease와 cross-context invalidate-only hint
|
||||
|
||||
reference list/detail은 bound definition이 key, executor와 profile을 함께
|
||||
제공한다. strict canonical codec은 depth/node/string/encoded-byte, cycle/shared
|
||||
reference, undefined/NaN/negative-zero, sparse array, accessor와 non-plain object를
|
||||
닫고 runtime-private opaque identity만 query key에 넣는다. profile의 stale/gc/
|
||||
refetch/retry owner와 result item/byte admission이 실제 hook에 적용된다.
|
||||
|
||||
current invalidation definition은 namespace당 singular topic이고 같은 topic의
|
||||
multi-namespace fan-out을 compose하지 못한다. 아래 bounded topic-set registry는
|
||||
target delta이며 현재 runtime 증거가 아니다.
|
||||
|
||||
production scope runtime은 session transition 즉시 old generation을 fence하고
|
||||
QueryClient cancel/clear 뒤 새 opaque scope를 발급한다. identity registry는 active
|
||||
lease, refcount, bounded LRU, canonical-byte/entry ceiling과 token collision 검사를
|
||||
scope별로 소유한다. mutation duplicate baseline도 scope exact semantic input만
|
||||
join하고 late result를 폐기한다.
|
||||
|
||||
ordered optimistic layer, conditional validator CAS sidecar와 bounded cursor chain은
|
||||
실행 가능한 test runtime까지 존재하지만 reference backend/definition에는 아직
|
||||
연결하지 않았다. 남은 부분은 account identity projection, logical-key serialization,
|
||||
HTTP 304 transaction, product optimistic membership/revision, infinite-query binding과
|
||||
effect certainty reconcile이다.
|
||||
|
||||
## 소유권
|
||||
|
||||
VD-13:
|
||||
|
||||
- `CacheScopeSnapshot`
|
||||
- ORIGIN/ACCOUNT/SESSION projection
|
||||
- QueryClient generation/fence/remount
|
||||
- strict canonical key codec 공통 구현
|
||||
- cross-tab wire와 durable namespace epoch
|
||||
- optional IndexedDB persistence와 restore
|
||||
- logout/account switch purge
|
||||
|
||||
VD-25:
|
||||
|
||||
- operation/application query definition binding
|
||||
- per-query freshness/gc/refetch/result budget
|
||||
- cache admission과 mapped value contract
|
||||
- cursor/infinite pagination
|
||||
- conditional revalidation integration
|
||||
- mutation concurrency, optimistic patch와 reconciliation
|
||||
- invalidation/seed policy
|
||||
- transport-independent error/stale behavior
|
||||
|
||||
VD-25는 VD-13의 scope snapshot/key codec을 소비하고 다른 epoch/fingerprint
|
||||
protocol을 만들지 않는다.
|
||||
|
||||
## 결정
|
||||
|
||||
### 1. TanStack Query가 유일한 기본 Server State owner다
|
||||
|
||||
REST, GraphQL과 gRPC-Web unary result는 transport-independent application
|
||||
projection으로 mapping된 뒤 TanStack Query memory cache에 들어갈 수 있다.
|
||||
|
||||
기본적으로 설치하지 않는다.
|
||||
|
||||
- Redux/Zustand server entity copy
|
||||
- Apollo/urql normalized cache
|
||||
- raw HTTP response cache wrapper
|
||||
- generated client SDK cache
|
||||
- custom Map singleton
|
||||
|
||||
GraphQL normalized cache가 실제로 필요하면 bounded context에서 TanStack
|
||||
operation-result cache를 대체하는 별도 ADR을 승인한다. 두 cache에 같은 entity를
|
||||
동시 write하지 않는다.
|
||||
|
||||
browser HTTP cache/Cache Storage, TanStack Query memory와 IndexedDB query
|
||||
persistence는 서로 다른 owner다.
|
||||
|
||||
### 2. Bound Query Definition
|
||||
|
||||
caller가 query key와 executor를 독립적으로 조립하지 않는다.
|
||||
|
||||
```text
|
||||
QueryDefinition<Input, Value>
|
||||
definitionId
|
||||
owner
|
||||
operationId
|
||||
inputCodec
|
||||
keyCodecId
|
||||
serverStateProfileId
|
||||
scopePersistencePolicyId
|
||||
resultContractId
|
||||
execute(validatedInput, executionContext)
|
||||
```
|
||||
|
||||
binding:
|
||||
|
||||
```text
|
||||
bindQuery(definition, rawInput, CacheScopeSnapshot)
|
||||
-> validate/canonicalize input
|
||||
-> derive branded query key
|
||||
-> resolve immutable policy
|
||||
-> freeze execute closure and captured generation
|
||||
-> BoundQuery<Value>
|
||||
```
|
||||
|
||||
presentation API:
|
||||
|
||||
```text
|
||||
useApplicationQuery(boundQuery)
|
||||
```
|
||||
|
||||
`queryKey`, `queryFn`, stale/gc/retry와 arbitrary TanStack option을 feature page에서
|
||||
따로 넘기지 않는다. escape hatch가 필요하면 새 profile을 먼저 등록한다.
|
||||
|
||||
### 3. Semantic query identity
|
||||
|
||||
query key는 VD-13의 단일 normative layout을 그대로 사용한다.
|
||||
|
||||
```text
|
||||
[
|
||||
"query",
|
||||
keySchemaVersion,
|
||||
scopeProjectionFingerprint,
|
||||
namespaceName,
|
||||
namespaceVersion,
|
||||
queryDefinitionVersion,
|
||||
canonicalSemanticInput
|
||||
]
|
||||
```
|
||||
|
||||
- namespace/key schema/version과 scope projection은 VD-13 profile-owned
|
||||
- query definition version과 semantic input projection은 VD-25 definition-owned
|
||||
- scope projection은 VD-13의 exact profile output
|
||||
- input은 strict codec의 plain immutable representation
|
||||
- REST URL/query string, ETag와 cursor raw value를 root identity에 넣지 않음
|
||||
- GraphQL document/persisted hash를 넣지 않음
|
||||
- protobuf bytes/generated message를 넣지 않음
|
||||
- presentation locale/formatted string을 넣지 않음
|
||||
|
||||
transport migration이 use-case/result meaning을 보존하면 semantic query family를
|
||||
유지할 수 있다. schema/mapper meaning, scope나 output identity가 바뀌면
|
||||
query-definition/release epoch를 바꾼다.
|
||||
|
||||
strict key codec은 다음을 거절한다.
|
||||
|
||||
- `undefined`, sparse array
|
||||
- NaN, Infinity, negative zero policy mismatch
|
||||
- bigint/symbol/function
|
||||
- Date/Map/Set/RegExp/typed array/native/class instance
|
||||
- accessor/proxy/prototype pollution key
|
||||
- cycle/shared-reference ambiguity
|
||||
- non-plain object
|
||||
- depth/node/part/string/encoded-byte ceiling 초과
|
||||
|
||||
query key에 PII/business ID를 직접 넣지 않는다. 필요한 resource identity는
|
||||
feature policy가 발급한 opaque bounded token으로 투영한다.
|
||||
|
||||
cursor와 command 동일성은 raw value나 충돌 가능 digest만으로 판정하지 않는다.
|
||||
|
||||
```text
|
||||
RuntimeIdentityTokenCodecV1
|
||||
canonicalCodecVersion
|
||||
maxCanonicalBytes
|
||||
maxInternEntries
|
||||
maxInternCanonicalBytes
|
||||
tokenEntropyBits >= 128
|
||||
lifetime = RUNTIME_SCOPE
|
||||
```
|
||||
|
||||
- strict length-prefixed typed canonical encoding이 exact equality source다.
|
||||
- runtime-private intern table이 canonical bytes를 opaque random token에
|
||||
일대일로 binding하고 token collision을 reverse map으로 검사한다.
|
||||
- 같은 token 후보가 다른 canonical bytes와 충돌하면 새 token을 발급한다. bounded
|
||||
재시도 후에도 해결되지 않으면 `IDENTITY_TOKEN_COLLISION`으로 admission/join을
|
||||
fail-closed한다.
|
||||
- intern row는 lease/refcount를 가진다. Query entry가 설치된 동안, active
|
||||
observer/fetch와 mutation/join이 진행되는 동안 해당 token을 eviction하지 않는다.
|
||||
- Query removal/GC에서 query token lease를, mutation terminal/join waiter
|
||||
settlement에서 command token lease를 exact once release한다. runtime/scope
|
||||
close는 남은 table을 전부 폐기한다.
|
||||
- refcount 0 row만 bounded LRU eviction할 수 있다. entry 수 또는 total canonical
|
||||
bytes ceiling을 active lease 때문에 회수할 수 없으면
|
||||
`IDENTITY_INTERN_LIMIT_EXCEEDED`로 신규 cache/command admission을 fail-closed한다.
|
||||
- canonical bytes/raw cursor/command input은 query key, diagnostics,
|
||||
cross-context wire와 persistence에 넣지 않고 runtime/scope close에서 폐기한다.
|
||||
- token은 backend idempotency key, authorization proof나 durable identity가 아니다.
|
||||
|
||||
### 4. ServerStateProfile
|
||||
|
||||
```text
|
||||
ServerStateProfileV1
|
||||
profileId
|
||||
classification
|
||||
scopePersistencePolicyId
|
||||
staleTimeMs
|
||||
gcTimeMs
|
||||
refetchOnMount
|
||||
refetchOnFocus
|
||||
refetchOnReconnect
|
||||
networkMode
|
||||
retryOwner = TRANSPORT | QUERY | NONE
|
||||
maxResultItems
|
||||
maxEstimatedResultBytes
|
||||
paginationProfileId | null
|
||||
conditionalProfileId | null
|
||||
placeholderPolicy
|
||||
initialFailurePolicy
|
||||
refreshFailurePolicy
|
||||
authorizationFailurePolicy
|
||||
contractFailurePolicy
|
||||
invalidationTopicRefs[] = { topicId, topicVersion }
|
||||
owner
|
||||
```
|
||||
|
||||
implementation ceilings:
|
||||
|
||||
- query `invalidationTopicRefs` set/version은 joined VD-13
|
||||
`QueryScopePersistencePolicy`와 exact match
|
||||
- `gcTimeMs`는 inactive retention이고 `staleTimeMs`는 freshness이므로
|
||||
`staleTimeMs <= gcTimeMs`를 일반 불변조건으로 강제하지 않는다.
|
||||
- VD-13 persistence를 선택한 경우에만 restore `maxAgeMs`, retention과
|
||||
`gcTimeMs`의 join compatibility를 검증한다.
|
||||
- finite gc 기본, `Infinity`는 explicit immortal-static profile만
|
||||
- stale/gc 최대값
|
||||
- result item/estimated byte 상한
|
||||
- maximum pages
|
||||
- refetch trigger storm coalescing
|
||||
- foreground/background concurrency
|
||||
|
||||
현재 global 30초/5분은 reference default이지 모든 product query의 production
|
||||
정책이 아니다.
|
||||
|
||||
### 5. Retry owner
|
||||
|
||||
한 network operation에는 retry owner가 정확히 하나다.
|
||||
|
||||
| 상황 | 기본 owner |
|
||||
| --- | --- |
|
||||
| installed REST | REST transport |
|
||||
| persisted GraphQL | GraphQL transport |
|
||||
| Connect unary | Connect adapter 또는 selected edge 중 exact one |
|
||||
| gRPC-Web unary | gRPC-Web transport |
|
||||
| pure local query computation | Query 또는 none |
|
||||
|
||||
transport retry가 있는 definition은 TanStack `retry=false`다. Query retry callback이
|
||||
same gateway call을 다시 실행해 transport attempts를 배가하지 않는다.
|
||||
|
||||
manual UI retry는 새 logical query execution이다. keyed command의 ambiguous outcome을
|
||||
query retry처럼 재실행하지 않는다.
|
||||
|
||||
### 6. Cache admission
|
||||
|
||||
query cache에 admission 가능한 값:
|
||||
|
||||
- VD-24 mapper가 만든 immutable plain application read model
|
||||
- exact result contract/version
|
||||
- current scope/runtime generation
|
||||
- item/estimated-byte ceiling 안
|
||||
- complete result 또는 operation이 허용한 typed partial result
|
||||
|
||||
금지:
|
||||
|
||||
- raw JSON/GraphQL envelope
|
||||
- generated protobuf message/client
|
||||
- Response/ReadableStream
|
||||
- auth/CSRF/idempotency/cursor/validator/trace metadata
|
||||
- thrown Error/AppFailure detail payload
|
||||
- function/class/domain service/native object
|
||||
|
||||
admission 순서:
|
||||
|
||||
```text
|
||||
transport success
|
||||
-> schema
|
||||
-> mapper
|
||||
-> result budget
|
||||
-> scope/generation fence
|
||||
-> Query commit
|
||||
```
|
||||
|
||||
어느 단계든 실패하면 cache write 0회다.
|
||||
|
||||
### 7. Result size
|
||||
|
||||
`maxEstimatedResultBytes`는 memory reservation이 아니라 hard admission guard다.
|
||||
|
||||
- mapper output plain data를 bounded estimator로 측정
|
||||
- string UTF-8 bytes, key overhead, array/object node count를 보수적으로 합산
|
||||
- cycle/class/native/accessor는 측정 전에 거절
|
||||
- 측정 자체가 deadline/node ceiling을 넘으면 admission 거절
|
||||
- result cap을 넘겨도 transport success를 unbounded UI state로 반환하지 않고
|
||||
`RESULT_LIMIT_EXCEEDED` 또는 server pagination requirement로 닫음
|
||||
|
||||
large binary/collection은 streaming/file 또는 cursor page capability로 이동한다.
|
||||
|
||||
### 8. Query lifecycle
|
||||
|
||||
```text
|
||||
IDLE
|
||||
-> LOADING
|
||||
-> SUCCESS | EMPTY | TERMINAL_ERROR
|
||||
|
||||
SUCCESS | EMPTY
|
||||
-> REFRESHING
|
||||
-> SUCCESS | EMPTY
|
||||
-> STALE_DEGRADED
|
||||
-> TERMINAL/REAUTH when policy forbids stale visibility
|
||||
```
|
||||
|
||||
current AsyncState의 base와 overlay 구분을 유지한다.
|
||||
|
||||
failure policy:
|
||||
|
||||
| failure | default |
|
||||
| --- | --- |
|
||||
| transient network/5xx refresh failure | valid previous data + stale-degraded |
|
||||
| caller/navigation abort | terminal error로 표시하지 않음 |
|
||||
| auth required | sensitive/account query는 stale 숨김, reauth |
|
||||
| forbidden/account switch | current scope data 즉시 숨김/clear |
|
||||
| schema/mapper/contract mismatch | cache write 금지, default stale 숨김 또는 explicit safe-static exception |
|
||||
| rate limit | previous data 정책 + retry-after UX |
|
||||
| not found | feature policy에 따라 empty/remove/tombstone |
|
||||
|
||||
query profile이 sensitive stale data를 계속 보여 주는 결정을 global fallback으로
|
||||
상속하지 않는다.
|
||||
|
||||
### 9. Freshness와 refetch
|
||||
|
||||
`staleTime`은 business correctness/authorization TTL이 아니다.
|
||||
|
||||
- focus/reconnect/mount refetch는 profile별
|
||||
- simultaneous trigger는 one in-flight query로 coalesce
|
||||
- minimum refetch interval과 deadline 적용
|
||||
- visibility offline state는 hint이며 server revision을 대체하지 않음
|
||||
- response age/cache-control을 staleTime으로 자동 변환하지 않음
|
||||
- backend push/invalidation은 stale hint이며 authoritative refetch를 시작
|
||||
|
||||
freshness-sensitive command/read-after-write는 mutation receipt/revision 또는
|
||||
authoritative refetch contract를 사용한다.
|
||||
|
||||
### 10. Conditional revalidation
|
||||
|
||||
REST app-managed ETag profile만 internal validator metadata를 사용할 수 있다.
|
||||
|
||||
```text
|
||||
ValidatorBinding
|
||||
query definition/fingerprint
|
||||
scope fingerprint
|
||||
runtime generation
|
||||
representation version
|
||||
opaque validator
|
||||
```
|
||||
|
||||
- raw validator는 query value/key/diagnostics에 넣지 않음
|
||||
- 304는 exact binding + existing mapped cache value가 있을 때만 fresh transition
|
||||
- 304 freshness transition은 같은 query-entry cache revision에 CAS가 성공할 때만
|
||||
commit하며, concurrent 200/removal 뒤의 late 304를 폐기
|
||||
- value가 없거나 wrong generation이면 304 success로 만들지 않음
|
||||
- validator mismatch/full 200은 normal schema/mapper/admission을 다시 수행하고
|
||||
mapped value commit과 validator install/update를 하나의 entry transaction으로
|
||||
취급
|
||||
- Query removal/GC, scope/logout/account switch, release/contract/schema/mapper
|
||||
epoch 변경과 incompatible cache clear에서 validator sidecar도 함께 폐기
|
||||
- ordinary invalidation 때 validator를 conditional refetch까지 보존할지 즉시
|
||||
폐기할지는 profile이 고정하며 query entry와 독립적으로 남기지 않음
|
||||
|
||||
GraphQL/gRPC metadata를 arbitrary ETag로 해석하지 않는다. application revision
|
||||
field를 schema/mapper가 명시적으로 제공한 경우 별도 revalidation policy가
|
||||
사용한다.
|
||||
|
||||
### 11. Cursor pagination
|
||||
|
||||
```text
|
||||
PaginationProfile
|
||||
CURSOR_SINGLE_PAGE
|
||||
CURSOR_INFINITE
|
||||
OFFSET_STABLE
|
||||
```
|
||||
|
||||
cursor page:
|
||||
|
||||
```text
|
||||
CursorPage<T>
|
||||
items
|
||||
nextCursor | null
|
||||
hasMore
|
||||
snapshotToken | null
|
||||
```
|
||||
|
||||
page invariant:
|
||||
|
||||
- `hasMore === (nextCursor !== null)`을 codec에서 강제한다.
|
||||
- page item count는 requested/implementation ceiling 이하다.
|
||||
- chain 안의 snapshot token은 provider profile이 허용한 null/동일 값만 사용한다.
|
||||
- `hasMore=true`인 empty/non-progress page는 explicit sparse-page profile이 없으면
|
||||
contract failure다.
|
||||
|
||||
`CURSOR_INFINITE` root key:
|
||||
|
||||
- filters/sort/page size semantics
|
||||
- scope
|
||||
- page definition version
|
||||
- cursor 제외
|
||||
|
||||
`CURSOR_INFINITE` page parameter:
|
||||
|
||||
- adapter-private/opaque bounded cursor
|
||||
- previous page/snapshot binding
|
||||
- raw cursor를 diagnostics/URL state/persistence에 임의 저장하지 않음
|
||||
|
||||
`CURSOR_SINGLE_PAGE`:
|
||||
|
||||
- first page의 null marker 또는 current cursor의 runtime-scoped non-reversible
|
||||
identity token을 `canonicalSemanticInput`에 포함한다.
|
||||
- raw cursor는 bound executor closure에만 두며 query key/value/diagnostics에 넣지
|
||||
않는다.
|
||||
- runtime-private exact equality guard/token binding이 실패하면 single-page cache
|
||||
admission을 끄고 closed failure로 끝낸다.
|
||||
- runtime-scoped token을 쓰는 `CURSOR_SINGLE_PAGE`는 `MEMORY_ONLY`다.
|
||||
|
||||
infinite policy:
|
||||
|
||||
- max pages
|
||||
- max total items
|
||||
- max estimated bytes
|
||||
- repeated cursor/non-progress/loop detection
|
||||
- page eviction direction
|
||||
- refresh strategy: first page only, visible window 또는 complete bounded chain
|
||||
- item stable identity/duplicate/revision conflict policy
|
||||
- snapshot changed 시 old/new page를 섞지 않고 restart
|
||||
|
||||
pagination persistence는 기본 disabled다. `CURSOR_SINGLE_PAGE`를 durable하게
|
||||
만들려면 stable partition-bound keyed codec/key lifecycle을 별도 ADR로 승인해야
|
||||
하며 runtime token을 persistence key로 재사용하지 않는다. `CURSOR_INFINITE`
|
||||
persistence를 선택하려면 VD-13 profile이 cursor와 snapshot의
|
||||
classification/expiry, maximum persisted pages/bytes, restored `pageParams` 사용
|
||||
여부를 명시적으로 승인해야 한다. raw sensitive cursor 또는 만료 후 page
|
||||
parameter를 IndexedDB에 저장하지 않는다.
|
||||
|
||||
offset pagination은 insert/delete drift를 허용하는 dataset에서 사용하지 않는다.
|
||||
|
||||
### 12. Mutation Definition
|
||||
|
||||
```text
|
||||
MutationDefinition<Input, Value>
|
||||
definitionId
|
||||
operationId
|
||||
inputCodec
|
||||
logicalKeyCodec
|
||||
commandIdentityTokenCodecId
|
||||
concurrencyPolicy
|
||||
duplicatePolicy
|
||||
optimisticPolicyId | null
|
||||
invalidationTopicRefs[] = { topicId, topicVersion }
|
||||
seedPolicyId | null
|
||||
conflictPolicyId
|
||||
effectCertaintyPolicy
|
||||
owner
|
||||
```
|
||||
|
||||
presentation:
|
||||
|
||||
```text
|
||||
useApplicationMutation(boundMutation)
|
||||
```
|
||||
|
||||
caller는 raw optimistic query key/update function과 invalidation topic을 조립하지
|
||||
않는다.
|
||||
|
||||
mutation topic ref는 unique 0..16개이고 모두 VD-13 global topic registry의 exact
|
||||
version으로 resolve돼야 한다. 한 ref가 가리키는 bounded namespace set만
|
||||
invalidate하며 caller가 runtime에 topic을 추가하지 못한다.
|
||||
|
||||
### 13. Mutation concurrency
|
||||
|
||||
```text
|
||||
ConcurrencyPolicy
|
||||
PARALLEL
|
||||
SERIAL_BY_LOGICAL_KEY
|
||||
SUPERSEDE_PENDING_READ_BY_LOGICAL_KEY
|
||||
REJECT_WHILE_ACTIVE_BY_LOGICAL_KEY
|
||||
|
||||
DuplicatePolicy
|
||||
JOIN_IDENTICAL
|
||||
REJECT_DUPLICATE
|
||||
ALLOW_INDEPENDENT
|
||||
```
|
||||
|
||||
- logical key는 validated input의 approved opaque identity
|
||||
- command identity token은 operation/definition version, current scope
|
||||
partition과 **전체 validated semantic input**을
|
||||
`RuntimeIdentityTokenCodecV1`으로 intern해 만든다. UI transient field와
|
||||
transport bytes/idempotency key는 canonical equality source에 포함하지 않는다.
|
||||
- logical key는 serialization/conflict group이고 exact canonical equality +
|
||||
identity token은 동일 command 판별 값이다. 두 값을 서로 대체하지 않는다.
|
||||
- `JOIN_IDENTICAL`은 exact equality guard도 통과한 같은 identity token의 기존
|
||||
in-flight Promise와 terminal result를 공유하며 transport/optimistic layer를
|
||||
추가하지 않는다.
|
||||
- `REJECT_DUPLICATE`는 exact-identical token이 active이면 fetch 0회와 closed
|
||||
`DUPLICATE_IN_FLIGHT`를 반환한다.
|
||||
- `ALLOW_INDEPENDENT`는 같은 exact identity도 독립 command로 실행한다. backend
|
||||
replay/idempotency와 UX가 이를 명시적으로 허용한 operation에만 등록한다.
|
||||
- distinct input을 같은 Promise에 join하지 않음
|
||||
- same hook, two hooks, two routes의 coordinator가 동일 policy를 사용
|
||||
- coordinator는 hook-local singleton이 아니라 runtime/scope 수명의 registry-owned
|
||||
service이며 scope generation 전환에서 신규 admission을 닫고 late commit을 fence
|
||||
- `SUPERSEDE`는 이미 server로 보낸 non-replayable command를 cancel/rollback했다고
|
||||
가정하지 않음
|
||||
- local serialization은 server idempotency/concurrency authority가 아님
|
||||
- scope/generation change는 pending result commit을 fence
|
||||
|
||||
### 14. Optimistic patch
|
||||
|
||||
snapshot 전체 restore만 사용하지 않는다.
|
||||
|
||||
```text
|
||||
OptimisticLayer
|
||||
mutationId
|
||||
logicalKey
|
||||
commandIdentityToken
|
||||
baseCacheRevision
|
||||
expectedEntityRevision | null
|
||||
patch
|
||||
inversePatch
|
||||
affectedQueryDefinitions
|
||||
```
|
||||
|
||||
선택 가능한 구현:
|
||||
|
||||
- cache entry revision CAS
|
||||
- ordered optimistic layer log
|
||||
- operation-specific compare-and-apply patch
|
||||
|
||||
공통:
|
||||
|
||||
1. affected exact queries cancel
|
||||
2. bounded current revision/value 확인
|
||||
3. registered pure patch 적용
|
||||
4. other mutation layer와 ordering 보존
|
||||
5. failure에서 자기 layer만 제거/역적용
|
||||
6. success result/revision과 reconcile
|
||||
7. invalidation/refetch
|
||||
|
||||
old whole snapshot을 복원해 다른 mutation commit을 덮지 않는다.
|
||||
|
||||
optimistic update를 하지 않는 조건:
|
||||
|
||||
- snapshot/patch/result byte ceiling 초과
|
||||
- cache entry missing/wrong revision
|
||||
- non-deterministic merge
|
||||
- high-conflict command
|
||||
- scope/generation transition
|
||||
- unknown effect certainty
|
||||
|
||||
그 경우 pending UX만 보여 주고 server response/refetch를 기다린다.
|
||||
|
||||
### 15. Effect certainty와 conflict
|
||||
|
||||
```text
|
||||
MutationEffect
|
||||
NOT_APPLIED
|
||||
COMMITTED
|
||||
UNKNOWN
|
||||
```
|
||||
|
||||
- timeout/cancel/network failure가 `NOT_APPLIED`를 자동 의미하지 않음
|
||||
- keyed backend status/receipt가 있어야 ambiguous command reconcile 가능
|
||||
- `UNKNOWN`은 새 idempotency key로 자동 retry 금지
|
||||
- 409 business conflict, REST 412, GraphQL safe conflict code, gRPC `ABORTED`를
|
||||
common conflict surface로 mapping하되 의미 차이는 feature policy가 소유
|
||||
- server revision/merge/overwrite decision은 cache가 아니라 use case 소유
|
||||
|
||||
### 16. Mutation success, seed와 invalidation
|
||||
|
||||
server commit 뒤:
|
||||
|
||||
- exact returned result를 schema/mapper/fence/budget 검증
|
||||
- registered detail seed policy가 있으면 exact current entity/revision만 write
|
||||
- list/aggregate는 default invalidate
|
||||
- list patch는 deterministic sort/filter/membership policy가 있을 때만
|
||||
- invalidation topic은 query key가 아닌 opaque registry identity
|
||||
- current mutation lease 동안 remote hints coalesce
|
||||
|
||||
local invalidation failure는 committed command를 failure로 바꾸지 않는다.
|
||||
cache health를 degraded로 기록하고 bounded authoritative refetch를 예약한다.
|
||||
|
||||
### 17. Cross-context
|
||||
|
||||
현재 cross-tab wire는 invalidate-only다. 유지한다.
|
||||
|
||||
- query data/key/input/cursor/validator를 broadcast하지 않음
|
||||
- remote event는 authority가 아니라 stale hint
|
||||
- account/scope/version 검증은 VD-13
|
||||
- mutation ordering/optimistic layer를 tab 간 복제하지 않음
|
||||
- sequence gap은 모든 registered namespace를 stale 처리하되 active query만
|
||||
bounded refetch한다. inactive query는 다음 mount/focus에서 revalidate하고,
|
||||
persistence가 선택된 경우 durable ledger refresh는 VD-13 절차를 따른다.
|
||||
|
||||
### 18. GraphQL과 normalized cache
|
||||
|
||||
persisted GraphQL query도 mapped operation result를 TanStack에 cache한다.
|
||||
|
||||
- query key는 semantic application input
|
||||
- GraphQL document/hash는 key/value에 없음
|
||||
- GraphQL SDK cache는 `no-cache`/disabled
|
||||
- partial data default reject
|
||||
- approved partial result는 completeness metadata를 application contract가 소유
|
||||
- missing/error field를 previous complete value와 자동 merge하지 않음
|
||||
|
||||
normalized entity cache가 필요하면:
|
||||
|
||||
- bounded context 하나가 TanStack operation cache를 대체
|
||||
- key fields/typename, eviction, pagination merge, optimistic layer, logout/scope,
|
||||
persistence와 removal을 별도 ADR
|
||||
- dual write/read 금지
|
||||
|
||||
현재 `NOT_SELECTED`다.
|
||||
|
||||
### 19. Connect/gRPC-Web server stream
|
||||
|
||||
ordinary Query는 terminal operation 결과를 전제로 한다.
|
||||
|
||||
- unary는 normal query 가능
|
||||
- finite server stream을 complete aggregate로 쓸 경우 staging buffer에 bounded
|
||||
accumulate하고 valid Connect EndStream 또는 gRPC-Web terminal status,
|
||||
schema/mapper/fence 뒤 atomic cache commit
|
||||
- long-running stream은 `ServerStreamPort`와 registered reducer/invalidation owner
|
||||
- frame마다 query cache를 append하여 unbounded event history를 만들지 않음
|
||||
- stream reconnect를 Query retry로 하지 않음
|
||||
- gap/overflow는 current snapshot 폐기 또는 authoritative query refetch
|
||||
|
||||
### 20. Persistence, SSR와 offline
|
||||
|
||||
- memory cache runtime은 `COMPOSED`
|
||||
- IndexedDB query persister reference는 VD-13 기준
|
||||
`DESIGNED_NOT_IMPLEMENTED`
|
||||
- product persistence는 `NOT_SELECTED`
|
||||
- SSR hydration은 `NOT_SELECTED`
|
||||
- offline mutation queue는 `NOT_SELECTED`
|
||||
|
||||
VD-25 profile은 persistence를 직접 켜지 않는다. joined VD-13
|
||||
scope/persistence profile, reference runtime과 제품 allowlist/retention/scope가
|
||||
모두 구현·선택된 query만 IndexedDB persistence를 사용할 수 있다.
|
||||
|
||||
server-state policy를 이유로 Web Storage에 query payload를 넣지 않는다.
|
||||
|
||||
### 21. Security와 privacy
|
||||
|
||||
- authorization result를 staleTime/cache hit으로 대체하지 않음
|
||||
- account/logout transition에서 sensitive data를 즉시 fence/hide
|
||||
- query key에 raw PII/account/resource ID/URL/document/message 금지
|
||||
- cache value에 credential/header/validator/trace/error raw data 금지
|
||||
- optimistic layer에도 command token/raw body를 저장하지 않음
|
||||
- command identity token/logical key를 diagnostics, cross-context wire나 persistence에
|
||||
넣지 않음
|
||||
- developer tools/diagnostics production exposure policy
|
||||
- cache poisoning 방지를 위해 schema/mapper/result contract와 generation 검증
|
||||
- cross-context event에 data 없음
|
||||
|
||||
### 22. Observability
|
||||
|
||||
허용:
|
||||
|
||||
- query/mutation definition/profile ID
|
||||
- hit/miss/stale/fresh/refresh/evict outcome
|
||||
- result item/estimated-byte/page bucket
|
||||
- runtime identity intern entry/canonical-byte/active-lease bucket
|
||||
- focus/reconnect/invalidation refetch reason
|
||||
- mutation concurrency/duplicate/optimistic/rollback/conflict/effect bucket
|
||||
- invalidation/seed/degraded recovery outcome
|
||||
- scope/profile version의 low-cardinality bucket
|
||||
|
||||
금지:
|
||||
|
||||
- query key/input/value
|
||||
- command identity token/logical key
|
||||
- identity intern canonical bytes/token actual value
|
||||
- cursor/snapshot/validator/revision actual value
|
||||
- resource/account/tenant ID
|
||||
- GraphQL/protobuf/REST DTO
|
||||
- optimistic patch/snapshot
|
||||
|
||||
### 23. Testing
|
||||
|
||||
query definition/key:
|
||||
|
||||
- definition/input/key/executor type/runtime binding
|
||||
- VD-13↔VD-25 topic set/version exact join과 bounded many-to-many fan-out
|
||||
- runtime identity token same-input stability, random-token collision regeneration과
|
||||
bounded failure의 cache/join 0회
|
||||
- intern entry/total-byte ceiling, active non-eviction, Query GC/mutation terminal
|
||||
lease release와 runtime-close leak 0
|
||||
- undefined/NaN/Date/class/accessor/cycle/sparse/oversize collision fixture
|
||||
- same semantic input stable identity
|
||||
- protocol wire identity 변화가 key에 들어가지 않음
|
||||
- per-profile stale/gc/refetch/retry owner
|
||||
|
||||
cache admission:
|
||||
|
||||
- mapped immutable plain value only
|
||||
- item/estimated byte cap
|
||||
- schema/mapper/generation failure write 0
|
||||
- auth/contract failure stale visibility
|
||||
|
||||
mutation:
|
||||
|
||||
- same semantic command identity, same logical key의 distinct input
|
||||
- same hook/two hooks/two routes
|
||||
- runtime/scope coordinator의 logical-key serial/parallel과
|
||||
join/reject/independent duplicate 결과
|
||||
- out-of-order success/failure
|
||||
- optimistic layer/CAS rollback without overwriting other commit
|
||||
- effect `NOT_APPLIED/COMMITTED/UNKNOWN`
|
||||
- server response detail seed + list invalidate
|
||||
- invalidation failure after commit
|
||||
- account switch during pending command
|
||||
|
||||
pagination:
|
||||
|
||||
- null/repeated/cyclic cursor
|
||||
- single-page cursor identity-token collision/isolation과 memory-only enforcement
|
||||
- `hasMore`/`nextCursor` 불일치와 snapshot drift
|
||||
- snapshot change
|
||||
- max page/item/byte
|
||||
- page eviction/refetch
|
||||
- duplicate identity/revision policy
|
||||
- cancellation and late page
|
||||
|
||||
integration:
|
||||
|
||||
- REST/GraphQL/gRPC unary → schema → mapper → cache → UI
|
||||
- focus/reconnect/offline/stale-degraded
|
||||
- conditional 304 exact binding
|
||||
- cross-tab invalidation/mutation lease
|
||||
- logout/account/release generation
|
||||
- finite stream atomic commit and overflow
|
||||
|
||||
### 24. Rollout
|
||||
|
||||
1. VD-13 strict key codec/scope snapshot interface를 확정한다.
|
||||
2. current `useApplicationQuery({queryKey, execute})` 뒤에 bound definition adapter를
|
||||
추가한다.
|
||||
3. reference queries를 shadow key/policy로 비교하되 secondary cache write 금지.
|
||||
4. per-profile policy/result ceiling을 read query에 canary한다.
|
||||
5. typed mutation definition과 input-aware coordinator를 도입한다.
|
||||
6. optimistic layer/CAS를 low-conflict command에만 canary한다.
|
||||
7. cursor page reference vertical을 구현한다.
|
||||
8. arbitrary key/executor와 raw optimistic callback API를 제거한다.
|
||||
9. account/generation browser evidence와 runbook drill 뒤 traffic을 올린다.
|
||||
|
||||
rollback:
|
||||
|
||||
- 신규 query/mutation admission/optimistic patch를 kill switch로 닫음
|
||||
- current scope queries cancel
|
||||
- unsafe/incompatible memory cache clear
|
||||
- pending command effect certainty reconcile
|
||||
- current basic facade 또는 no-optimistic authoritative refetch로 downgrade
|
||||
- schema/mapper/query definition/backend artifact를 coherent set으로 복구
|
||||
|
||||
## 규범 기준
|
||||
|
||||
- [TanStack Query v5 Important Defaults](https://tanstack.com/query/v5/docs/framework/react/guides/important-defaults)
|
||||
- [TanStack Query v5 Query Cancellation](https://tanstack.com/query/v5/docs/framework/react/guides/query-cancellation)
|
||||
|
||||
## 완료 기준
|
||||
|
||||
- caller가 arbitrary key/executor/TanStack option을 조합할 수 없다.
|
||||
- strict key codec과 VD-13 scope projection이 모든 query에 적용된다.
|
||||
- VD-13 normative key layout과 scope/persistence profile을 재정의하지 않고 exact
|
||||
join한다.
|
||||
- mapped/bounded/current-generation value만 cache에 들어간다.
|
||||
- transport와 Query retry owner가 중복되지 않는다.
|
||||
- cursor page가 next/snapshot/loop/page/item/byte ceiling을 갖는다.
|
||||
- runtime-private exact equality guard까지 통과한 command identity만 declared
|
||||
join되고 distinct input이 같은 Promise로 잘못 join되지 않는다.
|
||||
- concurrent optimistic rollback이 다른 committed update를 덮지 않는다.
|
||||
- effect certainty, conflict, seed와 invalidation owner가 operation별로 닫힌다.
|
||||
- GraphQL normalized dual cache와 unbounded stream cache가 없다.
|
||||
- scope/logout/provider fault와 rollback/removal evidence가 통과한다.
|
||||
@@ -0,0 +1,653 @@
|
||||
# VD-26: Persisted GraphQL operation
|
||||
|
||||
- 상태: Accepted design — reference runtime implementation pending
|
||||
- 결정일: 2026-07-28
|
||||
- provider-neutral GraphQL reference adapter:
|
||||
`DESIGNED_NOT_IMPLEMENTED`
|
||||
- product GraphQL composition: `NOT_SELECTED`
|
||||
- batching/subscription/`@defer`/`@stream`: `NOT_SELECTED`
|
||||
- normalized GraphQL cache: `NOT_SELECTED`
|
||||
- 관련 결정: VD-13, VD-23, VD-24, VD-25, VD-28
|
||||
- 상세 설계:
|
||||
[API contract, Schema, Mapper와 Server State](../api-contract-schema-mapper-and-server-state.md)
|
||||
|
||||
## 배경
|
||||
|
||||
현재 source, package direct dependency, config와 test에는 GraphQL runtime,
|
||||
operation document/codegen, persisted manifest나 endpoint provider가 없다.
|
||||
lockfile의 transitive `graphql` package는 MSW 개발 의존성일 뿐 capability
|
||||
구현 증거가 아니다.
|
||||
|
||||
GraphQL은 임의 query string을 보내는 범용 API escape hatch로 도입하지 않는다.
|
||||
제품이 여러 backend aggregate를 화면별 shape로 조회해야 하고 schema/router,
|
||||
field authorization, persisted allowlist와 cost budget을 운영할 수 있을 때만
|
||||
bounded-context operation family로 선택한다.
|
||||
|
||||
## 결정
|
||||
|
||||
### 1. Production GraphQL은 persisted operation only다
|
||||
|
||||
```text
|
||||
semantic application query/command
|
||||
-> registered GraphqlOperationDefinition
|
||||
-> fixed endpoint
|
||||
-> persisted operation ID/hash
|
||||
-> validated variables
|
||||
-> bounded GraphQL response decoder
|
||||
-> operation data schema
|
||||
-> boundary mapper
|
||||
-> application projection
|
||||
-> TanStack Query or command result
|
||||
```
|
||||
|
||||
production runtime은 다음을 받지 않는다.
|
||||
|
||||
- arbitrary GraphQL document
|
||||
- caller-provided operation name/hash
|
||||
- arbitrary endpoint/header
|
||||
- generated SDK selection set builder
|
||||
- field/fragment string
|
||||
|
||||
### 2. Operation artifact
|
||||
|
||||
```text
|
||||
PersistedGraphqlOperationV1
|
||||
protocol = PERSISTED_GRAPHQL_V1
|
||||
semanticOperationId
|
||||
operationName
|
||||
operationKind = QUERY | MUTATION
|
||||
canonicalDocumentSha256
|
||||
persistedOperationId
|
||||
schemaArtifactId
|
||||
schemaDigest
|
||||
variablesSchemaId
|
||||
dataSchemaId
|
||||
mapperId
|
||||
errorProfileId
|
||||
partialDataPolicy
|
||||
endpointId
|
||||
graphqlHttpProfileRevision
|
||||
persistedEnvelopeProfileId
|
||||
responseStatusMediaProfileId
|
||||
authProfileId
|
||||
csrfProfileId
|
||||
replayPolicy
|
||||
deadlineProfileId
|
||||
retryProfileId
|
||||
serverStateProfileId | null
|
||||
maxVariablesBytes
|
||||
maxResponseBytes
|
||||
maxErrorCount
|
||||
maxCost
|
||||
maxDepth
|
||||
maxAliases
|
||||
owner
|
||||
```
|
||||
|
||||
canonical document는 build artifact이고 runtime string이 아니다. stable operation
|
||||
ID와 hash는 schema/operation manifest에 binding한다.
|
||||
|
||||
manifest 생성:
|
||||
|
||||
```text
|
||||
authenticated immutable schema
|
||||
-> named operation sources
|
||||
-> parse/validate against schema
|
||||
-> canonical document
|
||||
-> operation hash/ID
|
||||
-> variables/result type generation
|
||||
-> runtime codec manifest
|
||||
-> mapper/query profile binding
|
||||
-> persisted operation manifest
|
||||
```
|
||||
|
||||
### 3. Schema와 codegen
|
||||
|
||||
- schema source URL에서 normal build마다 latest를 받지 않는다.
|
||||
- authenticated explicit update workflow가 immutable artifact와 provenance를
|
||||
저장한다.
|
||||
- schema/source/operation manifest digest를 release contract set에 binding한다.
|
||||
- anonymous operation, duplicate operation name와 invalid fragment를 거절한다.
|
||||
- generator, plugins, Node와 runtime version을 pin한다.
|
||||
- clean checkout regenerate diff가 0이어야 한다.
|
||||
- schema breaking diff, operation validation, deprecated field budget와 generated
|
||||
output digest를 CI gate로 둔다.
|
||||
- generated type은 adapter-private DTO다.
|
||||
- generated TypeScript type만 믿지 않고 variables/data runtime codec과 mapper를
|
||||
유지한다.
|
||||
- schema introspection을 production에서 끄는 결정은 server 보안 옵션일 뿐
|
||||
authorization/cost control을 대체하지 않는다.
|
||||
|
||||
### 4. Endpoint와 HTTP profile
|
||||
|
||||
```text
|
||||
GraphqlProviderProfile
|
||||
endpointId
|
||||
fixedHttpsUrl
|
||||
graphqlHttpProfileRevision
|
||||
persistedEnvelopeProfileId
|
||||
methodPolicy
|
||||
credentialsMode
|
||||
corsProfile
|
||||
referrerPolicy
|
||||
redirect = ERROR
|
||||
mediaProfile
|
||||
```
|
||||
|
||||
GraphQL-over-HTTP draft를 움직이는 implicit `latest`로 구현하지 않는다. selected
|
||||
revision의 request/response/status 규칙과 provider의 persisted-operation
|
||||
extension을 immutable profile/fixture에 고정한다. persisted ID/hash-only envelope는
|
||||
표준 request의 required `query` field를 생략하는 provider extension일 수 있으므로
|
||||
generic GraphQL-over-HTTP compliance로 가장하지 않는다.
|
||||
|
||||
private query와 mutation은 POST가 기본이다.
|
||||
|
||||
GET은 다음을 모두 만족하는 public read profile에서만 선택한다.
|
||||
|
||||
- persisted ID/hash와 non-sensitive bounded variables
|
||||
- URL byte ceiling
|
||||
- no credential/private representation 또는 명시된 safe cache contract
|
||||
- exact cache key/Vary/CDN policy
|
||||
- mutation 아님
|
||||
|
||||
raw document와 sensitive variables를 URL에 넣지 않는다.
|
||||
|
||||
request `Content-Type: application/json`과
|
||||
`Accept: application/graphql-response+json`을 기본 exact profile로 둔다.
|
||||
`application/json` response 지원은 legacy provider profile로 분리한다. caller가
|
||||
`fetch` option, headers와 credentials를 override하지 않는다.
|
||||
|
||||
status/media matrix:
|
||||
|
||||
- final URL/origin과 media/body ceiling을 먼저 확인한다.
|
||||
- `application/graphql-response+json`은 profile이 허용한 HTTP status 전체에서
|
||||
bounded GraphQL envelope를 먼저 decode하고 selected revision의 status/body
|
||||
불변조건을 교차 검증한다.
|
||||
- non-null `data`가 있는 response는 selected revision이 요구하는 2xx여야 한다.
|
||||
no-data/error와 partial response의 status는 pinned revision/provider fixture와
|
||||
exact match해야 한다.
|
||||
- legacy `application/json`은 허용된 2xx body만 GraphQL envelope로 신뢰한다.
|
||||
non-2xx body는 intermediary일 수 있으므로 GraphQL error/extensions로
|
||||
해석하지 않고 bounded generic HTTP failure로 닫는다.
|
||||
|
||||
### 5. Request envelope
|
||||
|
||||
wire shape는 provider의 persisted-envelope extension이 versioned codec으로
|
||||
고정한다. 최소 의미:
|
||||
|
||||
```text
|
||||
protocol
|
||||
persisted operation ID
|
||||
canonical document hash
|
||||
operation name
|
||||
validated variables
|
||||
client contract manifest version
|
||||
```
|
||||
|
||||
full document는 포함하지 않는다.
|
||||
|
||||
provider가 ID/hash-only envelope를 지원하지 않으면 이 capability를 그 endpoint에
|
||||
compose하지 않는다. production에서 표준 `query` field를 채우기 위해 full
|
||||
document fallback을 보내는 것으로 우회하지 않는다.
|
||||
|
||||
variables:
|
||||
|
||||
- request runtime schema의 parsed output만 사용
|
||||
- unknown field 거절
|
||||
- depth/node/string/list/encoded byte ceiling
|
||||
- File/Blob/stream/native/generated class 금지
|
||||
- ID/decimal/int64/time semantics는 VD-24
|
||||
- secret/credential를 variable로 전달하는 operation 금지
|
||||
|
||||
### 6. APQ와 manifest miss
|
||||
|
||||
runtime Automatic Persisted Query negotiation을 production default로 사용하지
|
||||
않는다.
|
||||
|
||||
```text
|
||||
persisted miss/hash mismatch
|
||||
-> body/reader cancel
|
||||
-> PERSISTED_OPERATION_MISMATCH
|
||||
-> operation traffic disable or coherent manifest recovery
|
||||
```
|
||||
|
||||
hash miss 뒤 full document를 자동 전송하면 server allowlist와 cost governance를
|
||||
우회할 수 있다. trusted development profile에서만 explicit opt-in 가능하며
|
||||
production promotion 증거로 사용하지 않는다.
|
||||
|
||||
frontend manifest와 router manifest의 N/N-1 rollout을 먼저 증명한다.
|
||||
|
||||
### 7. Total deadline, cancellation과 retry
|
||||
|
||||
VD-23 common logical deadline을 사용한다.
|
||||
|
||||
- credential/CSRF attach
|
||||
- network attempts/backoff
|
||||
- response read/JSON parse
|
||||
- GraphQL envelope/data/error validation
|
||||
- mapper
|
||||
|
||||
Query retry와 GraphQL transport retry를 중복하지 않는다.
|
||||
|
||||
retry:
|
||||
|
||||
- idempotent query의 selected network/408/429/502/503/504
|
||||
- keyed mutation은 backend idempotency evidence가 있을 때만
|
||||
- GraphQL validation, persisted miss, cost/depth, schema/data/error mismatch는
|
||||
retry하지 않음
|
||||
- HTTP 200 GraphQL business error를 transient network failure로 자동 retry하지 않음
|
||||
- UNAUTHENTICATED recovery는 safe query/keyed mutation만 same logical binding으로
|
||||
한 번
|
||||
|
||||
AbortSignal은 fetch와 body/incremental reader를 cancel한다. local cancel이 mutation
|
||||
미적용을 의미하지 않으며 ambiguous effect는 status/reconcile contract로 닫는다.
|
||||
|
||||
### 8. Response decoder
|
||||
|
||||
```text
|
||||
HTTP response
|
||||
-> final URL/origin/media/header
|
||||
-> present/valid Content-Length advisory preflight
|
||||
-> bounded stream reader
|
||||
-> decoded byte/depth/node/string/list cap
|
||||
-> GraphQL response envelope
|
||||
-> pinned HTTP status/body matrix
|
||||
-> data/errors state machine
|
||||
-> operation data codec
|
||||
-> mapper
|
||||
```
|
||||
|
||||
top-level:
|
||||
|
||||
```text
|
||||
GraphqlResponse
|
||||
data?
|
||||
errors?
|
||||
extensions?
|
||||
```
|
||||
|
||||
unknown top-level/extension behavior는 provider profile과 VD-24 unknown-field
|
||||
정책을 따른다. response body, error message, path와 extensions를 log에 복사하지
|
||||
않는다.
|
||||
|
||||
### 9. Data/error state machine
|
||||
|
||||
다음 순서로 배타적으로 처리한다.
|
||||
|
||||
1. network/final URL/unsupported media/body limit 실패 또는 legacy
|
||||
`application/json` non-2xx:
|
||||
transport 또는 media/limit failure, data/cache write 0.
|
||||
2. `application/graphql-response+json`은 profile이 허용한 status 전체에서,
|
||||
legacy `application/json`은 profile-admitted 2xx에서만 bounded parse한다.
|
||||
top-level response shape 불일치는 `GRAPHQL_ENVELOPE_MISMATCH`.
|
||||
3. `errors` key가 있으면 non-empty list여야 한다. `errors=[]`는 항상
|
||||
`GRAPHQL_ENVELOPE_MISMATCH`다.
|
||||
4. selected GraphQL-over-HTTP revision의 status/body matrix가 맞지 않으면
|
||||
`GRAPHQL_HTTP_PROFILE_MISMATCH`다.
|
||||
5. `data` key 존재 + non-null, `errors` 없음:
|
||||
data codec → mapper → generation fence → success.
|
||||
6. `data` 없음/null, non-empty `errors`:
|
||||
safe error mapping; success/cache write 0.
|
||||
7. non-null `data`와 non-empty `errors` 동시:
|
||||
operation `partialDataPolicy` 적용.
|
||||
8. `data` 없음/null이고 errors도 없음:
|
||||
contract mismatch.
|
||||
|
||||
### 10. Error projection
|
||||
|
||||
GraphQL error는 untrusted다.
|
||||
|
||||
```text
|
||||
GraphqlError
|
||||
message
|
||||
locations
|
||||
path
|
||||
extensions
|
||||
```
|
||||
|
||||
application에 허용:
|
||||
|
||||
- operation error profile이 allowlist한 `extensions.code`
|
||||
- bounded typed validation field issue
|
||||
- effect certainty/conflict category
|
||||
- bounded server request/trace ID projection
|
||||
|
||||
금지:
|
||||
|
||||
- raw `message`
|
||||
- source location
|
||||
- path actual value
|
||||
- arbitrary extensions
|
||||
- resolver/service/stack/database detail
|
||||
|
||||
error count, path segment/count/string와 extensions decoded byte cap을 적용한다.
|
||||
unknown code는 generic closed failure다. backend `retryable` boolean은 retry
|
||||
authority가 아니다.
|
||||
|
||||
common mapping 예:
|
||||
|
||||
| safe GraphQL category | AppFailure |
|
||||
| --- | --- |
|
||||
| unauthenticated | `AUTH_REQUIRED` |
|
||||
| forbidden | `FORBIDDEN` |
|
||||
| not found | `NOT_FOUND` 또는 existence-hiding policy |
|
||||
| validation | `VALIDATION_REJECTED` |
|
||||
| conflict/precondition | `CONFLICT` 또는 typed precondition |
|
||||
| rate limited | `RATE_LIMITED` |
|
||||
| internal/unavailable | `SERVER_FAILURE` |
|
||||
| unknown | `UNKNOWN_CLIENT_FAILURE` 또는 contract failure |
|
||||
|
||||
### 11. Partial data
|
||||
|
||||
default:
|
||||
|
||||
```text
|
||||
partialDataPolicy = REJECT
|
||||
```
|
||||
|
||||
query에만 다음 explicit profile을 허용할 수 있다.
|
||||
|
||||
```text
|
||||
ALLOW_TYPED_PARTIAL
|
||||
requiredCompletePaths
|
||||
optionalPartialPaths
|
||||
errorCodeAllowlist
|
||||
completenessSchemaId
|
||||
staleVisibilityPolicy
|
||||
```
|
||||
|
||||
조건:
|
||||
|
||||
- data codec이 missing/null path를 정확히 표현
|
||||
- mapper가 completeness를 application result로 투영
|
||||
- UI가 complete success와 partial-degraded를 구분
|
||||
- partial value/result size ceiling
|
||||
- authorization/error path를 숨기며 unsafe field를 사용하지 않음
|
||||
- previous complete cache와 field 단위로 임의 merge하지 않음
|
||||
|
||||
mutation은 errors가 있으면 partial success data를 ordinary command success로
|
||||
cache하지 않는다. backend가 effect certainty/receipt를 제공해야
|
||||
`COMMITTED | NOT_APPLIED | UNKNOWN`을 판단한다. error가 있다는 이유만으로
|
||||
optimistic layer 전체를 즉시 rollback해 다른 commit을 덮지 않는다.
|
||||
|
||||
### 12. Null bubbling
|
||||
|
||||
GraphQL nullability propagation은 application null 의미와 다르다.
|
||||
|
||||
- nullable field, error-caused null과 absent partial field를 data/error state
|
||||
machine이 함께 해석
|
||||
- generated type의 `T | null`만으로 cause를 추측하지 않음
|
||||
- operation data codec/mapper가 approved partial path와 error code를 결합
|
||||
- required root/aggregate null은 default failure
|
||||
- unauthorized field null을 stale previous field로 자동 채우지 않음
|
||||
|
||||
### 13. Cache identity
|
||||
|
||||
VD-25 TanStack Query가 기본 sole owner다.
|
||||
|
||||
- query key는 semantic operation input + VD-13 scope
|
||||
- persisted operation ID/hash/document를 key에 넣지 않음
|
||||
- GraphQL data/envelope/generated type을 cache하지 않음
|
||||
- mapped bounded application projection만 cache
|
||||
- schema/mapper meaning change는 query/release epoch invalidation
|
||||
- GraphQL client library cache는 disabled/`no-cache`
|
||||
|
||||
normalized cache가 필요하면 separate ADR:
|
||||
|
||||
- key fields/`__typename`
|
||||
- fragment completeness
|
||||
- pagination merge
|
||||
- optimistic layers
|
||||
- eviction/gc/logout/scope
|
||||
- persistence/SSR
|
||||
- TanStack replacement/removal
|
||||
|
||||
dual cache는 금지한다.
|
||||
|
||||
### 14. Batching
|
||||
|
||||
현재 `NOT_SELECTED`.
|
||||
|
||||
`@defer`/`@stream`은 한 GraphQL HTTP operation의 finite incremental response다.
|
||||
장기 subscription이나 unsolicited realtime event가 아니며, reconnect/resume
|
||||
owner를 realtime runtime에 넘기지 않는다.
|
||||
|
||||
선택 조건:
|
||||
|
||||
- 같은 endpoint/auth/scope
|
||||
- query only
|
||||
- same credentials/CSRF policy
|
||||
- max operation count
|
||||
- total variables/request bytes
|
||||
- total cost/depth
|
||||
- per-operation deadline/result/error/observation 보존
|
||||
- one operation cancel/failure가 다른 operation semantics를 바꾸지 않음
|
||||
|
||||
금지:
|
||||
|
||||
- mutation 포함
|
||||
- query+mutation mixed batch
|
||||
- 서로 다른 account/session
|
||||
- batching으로 idempotency/retry owner 합치기
|
||||
- one HTTP result를 one query cache value로 저장
|
||||
|
||||
batch transport failure와 per-operation GraphQL failure를 분리한다.
|
||||
|
||||
### 15. Incremental `@defer`/`@stream`
|
||||
|
||||
현재 `NOT_SELECTED`.
|
||||
|
||||
선택 시 별도 profile:
|
||||
|
||||
- exact incremental-delivery draft/provider revision
|
||||
- exact `Accept`, response `Content-Type`와 boundary/version parameter
|
||||
- exact `multipart/mixed` media/boundary parser
|
||||
- total bytes/parts/depth/patch count
|
||||
- initial/subsequent/terminal payload discriminant와 completion grammar
|
||||
- operation-owned ID/label/path allowlist와 path progression
|
||||
- patch/data/items/errors/extensions runtime schema
|
||||
- part별 및 cumulative error/extension count/byte ceiling
|
||||
- duplicate/out-of-order/missing path
|
||||
- terminal marker
|
||||
- idle/total deadline
|
||||
- backpressure/cancel/reader cleanup
|
||||
- proxy/CDN buffering conformance
|
||||
|
||||
cache:
|
||||
|
||||
- staging projection에 immutable patch 적용
|
||||
- terminal integrity/completeness 뒤 atomic commit
|
||||
- 또는 UI가 explicit progressive state를 소유
|
||||
- existing cached object를 in-place mutate하지 않음
|
||||
- truncated stream을 complete success로 cache하지 않음
|
||||
|
||||
Chromium/Firefox/WebKit과 actual proxy 증거 없이는 traffic promotion 금지다.
|
||||
exact protocol revision/profile이 없으면 registry composition 자체를 거절한다.
|
||||
|
||||
### 16. Subscription
|
||||
|
||||
GraphQL HTTP query adapter에 subscription을 넣지 않는다. 현재 `NOT_SELECTED`.
|
||||
|
||||
선택 시 transport-specific registered GraphQL subscription capability와
|
||||
feature-owned `FeatureEventInput`이 필요하다. 범용 `RealtimePort`를 만들지
|
||||
않는다.
|
||||
|
||||
```text
|
||||
GraphqlSubscriptionCapability
|
||||
subscribe(registered subscription, validated variables, signal)
|
||||
-> AsyncIterable<Result<MappedEvent>>
|
||||
-> unsubscribe()
|
||||
```
|
||||
|
||||
선택된 WebSocket/SSE subprotocol adapter가 frame, media, auth, reconnect/resume를
|
||||
소유하고 GraphQL event schema와 pure mapper를 통과한 event만
|
||||
`FeatureEventInput` 또는 invalidation bridge로 전달한다. backend contract가
|
||||
명시적으로 같은 의미를 채택하지 않는 한 GraphQL payload를
|
||||
`REALTIME_EVENT_V1`로 강제하거나 다시 감싸지 않는다.
|
||||
|
||||
backend 계약:
|
||||
|
||||
- exact WebSocket/SSE protocol/version
|
||||
- auth attach/refresh/revoke
|
||||
- heartbeat/idle timeout
|
||||
- reconnect/backoff
|
||||
- sequence/duplicate/gap/resume cursor
|
||||
- bounded queue/overflow
|
||||
- logout/route unmount unsubscribe
|
||||
|
||||
event는 invalidation hint 또는 registered bounded reducer를 통해 server-state를
|
||||
갱신한다. raw event history를 Query cache에 무한 적재하지 않는다.
|
||||
|
||||
### 17. Authorization, CSRF와 DoS
|
||||
|
||||
- BFF/router가 field/resource authorization을 매 request에 수행
|
||||
- persisted allowlist는 authorization이 아님
|
||||
- cookie mutation은 POST + exact Origin/Fetch Metadata + approved CSRF proof
|
||||
- SameSite/custom header/preflight 단일 요소만 방어라고 주장하지 않음
|
||||
- cross-origin credential wildcard 금지
|
||||
- server에서 depth, aliases, fragments, variables/list/page/field cost, total
|
||||
execution와 response bytes 강제
|
||||
- frontend ceiling은 server DoS 방어를 대체하지 않음
|
||||
- introspection off는 field authorization/cost control 대체 아님
|
||||
- persisted operation manifest와 field authorization change를 coherent rollout
|
||||
|
||||
### 18. Backend/router 계약
|
||||
|
||||
provider가 제공:
|
||||
|
||||
- immutable schema artifact/provenance
|
||||
- persisted operation registration/lookup
|
||||
- exact operation hash/schema digest binding
|
||||
- N/N-1 manifest window와 retirement
|
||||
- cost/depth/alias/list/response budget enforcement
|
||||
- stable safe error code vocabulary
|
||||
- partial/null/effect certainty semantics
|
||||
- idempotency/conflict/revision
|
||||
- auth/CSRF/CORS
|
||||
- request/trace projection
|
||||
- kill switch와 per-operation traffic
|
||||
|
||||
frontend manifest echo만으로 등록/authorization을 승인하지 않는다. router가
|
||||
server-owned manifest에서 operation binding을 재계산한다.
|
||||
|
||||
### 19. Observability
|
||||
|
||||
허용:
|
||||
|
||||
- semantic operation ID/persisted profile ID
|
||||
- schema/manifest compatibility outcome
|
||||
- full/partial/rejected/transport outcome
|
||||
- safe GraphQL error category
|
||||
- cost/depth/variables/response/error/part count bucket
|
||||
- duration/deadline/retry/auth recovery bucket
|
||||
- cache hit/stale/admission outcome
|
||||
|
||||
금지:
|
||||
|
||||
- document/hash actual value
|
||||
- variables/data
|
||||
- raw error message/path/extensions
|
||||
- field/resolver name high-cardinality label
|
||||
- account/resource/cursor/revision
|
||||
|
||||
server가 resolver-level telemetry를 소유한다. browser가 raw field trace를 수집하지
|
||||
않는다.
|
||||
|
||||
### 20. Testing
|
||||
|
||||
build/contract:
|
||||
|
||||
- schema source provenance/digest
|
||||
- schema lint/breaking/deprecation budget
|
||||
- named operation validation
|
||||
- canonical hash/manifest determinism
|
||||
- clean codegen diff
|
||||
- generated import boundary
|
||||
- variables/data codec parity
|
||||
- N/N-1 persisted manifest and retirement
|
||||
|
||||
runtime:
|
||||
|
||||
- unknown/hash mismatch, full-document fallback 0
|
||||
- variables depth/node/string/list/byte cap
|
||||
- GraphQL HTTP revision/media/status-body matrix와 legacy intermediary body
|
||||
- HTTP/media/body cap
|
||||
- all data/errors state branches
|
||||
- empty errors와 null/absent data matrix
|
||||
- error count/path/extensions cap/redaction
|
||||
- null bubbling
|
||||
- partial allowed/rejected/completeness
|
||||
- timeout/cancel/retry/auth recovery
|
||||
- mutation effect certainty/idempotency
|
||||
- scope/generation late result
|
||||
- cache admission/write 0 on failure
|
||||
|
||||
optional:
|
||||
|
||||
- batching mixed/mutation/limit rejection
|
||||
- multipart boundary/truncated/duplicate/out-of-order/terminal
|
||||
- subscription ordering/reconnect/resume/logout
|
||||
|
||||
provider/browser:
|
||||
|
||||
- actual BFF/router allowlist/cost/auth/CSRF/CORS
|
||||
- manifest rollout/retirement
|
||||
- proxy/CDN media/body behavior
|
||||
- Chromium/Firefox/WebKit for selected incremental/subscription capability
|
||||
|
||||
### 21. Rollout
|
||||
|
||||
1. product owner가 GraphQL이 필요한 bounded operation family를 승인한다.
|
||||
2. schema/router/manifest owner와 endpoint/auth/cost/error contract를 확정한다.
|
||||
3. provider-neutral codec/adapter/fake를 구현한다.
|
||||
4. generated source, boundary mapper와 TanStack query definition을 연결한다.
|
||||
5. REST current read와 GraphQL shadow read를 비교하되 shadow result는 UI/cache에
|
||||
쓰지 않는다.
|
||||
6. actual router conformance를 통과한다.
|
||||
7. `AVAILABLE_NOT_COMPOSED`에서 product composition behind
|
||||
`TrafficAdmission=DISABLED`로 이동한다.
|
||||
8. read-only internal canary 뒤 selected operation만 traffic을 올린다.
|
||||
9. mutation은 idempotency/effect certainty provider evidence 뒤 별도 canary한다.
|
||||
10. batching/incremental/subscription은 계속 `NOT_SELECTED` 또는 독립 gate다.
|
||||
|
||||
rollback:
|
||||
|
||||
- 신규 GraphQL operation admission 중지
|
||||
- in-flight query cancel, mutation effect reconcile
|
||||
- current scope GraphQL-mapped query cache clear
|
||||
- coherent frontend/schema/manifest/router rollback
|
||||
- approved REST read fallback이 있으면 새 logical read로 전환
|
||||
- arbitrary/full-document fallback 금지
|
||||
|
||||
### 22. Removal
|
||||
|
||||
1. operation traffic/registration retirement 시작
|
||||
2. query/subscription cancel과 mutation reconcile
|
||||
3. Query cache/invalidation listener clear
|
||||
4. operation/codec/mapper/query profile 제거
|
||||
5. generated files, GraphQL runtime/codegen dependencies 제거
|
||||
6. schema/operation manifest/config/endpoint 제거
|
||||
7. router persisted entries는 N/N-1 window 뒤 제거
|
||||
8. production module/dependency/SBOM/removal test 통과
|
||||
|
||||
## 규범 기준
|
||||
|
||||
- [GraphQL Specification, September 2025](https://spec.graphql.org/September2025/)
|
||||
- [GraphQL over HTTP draft](https://graphql.github.io/graphql-over-http/draft/)
|
||||
|
||||
GraphQL-over-HTTP 문서는 현재 draft이므로 링크의 moving text를 production
|
||||
profile로 쓰지 않고 위에서 결정한 revision/provider fixture로 고정한다.
|
||||
|
||||
## 완료 기준
|
||||
|
||||
- production에서 registered persisted operation 외 document가 전송되지 않는다.
|
||||
- schema/operation/codegen/runtime codec/mapper manifest가 digest로 연결된다.
|
||||
- variables/response/errors가 bounded runtime validation을 거친다.
|
||||
- persisted envelope extension과 GraphQL-over-HTTP revision/media/status matrix가
|
||||
actual router profile에 고정된다.
|
||||
- data/errors/partial/null/effect certainty 상태가 배타적으로 닫힌다.
|
||||
- auth/CSRF/cost/field authorization과 manifest N/N-1을 actual router에서 증명한다.
|
||||
- GraphQL SDK normalized cache와 TanStack dual cache가 없다.
|
||||
- query key/cache에 document/hash/envelope/generated DTO가 없다.
|
||||
- batching/incremental/subscription은 선택 전 설치되지 않는다.
|
||||
- kill switch, rollback과 dependency/manifest removal drill이 통과한다.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,640 @@
|
||||
# VD-28: Realtime events, Web Push와 bounded polling
|
||||
|
||||
- 상태: Accepted — reference runtime available, product implementation pending
|
||||
- 결정일: 2026-07-28
|
||||
- 관련 결정: VD-10, VD-13, VD-23, VD-24, VD-25, VD-26, VD-27, VD-29
|
||||
- 상세 설계:
|
||||
`docs/architecture/realtime-events-web-push-and-bounded-polling.md`
|
||||
- 현재 product selection: `NOT_SELECTED`
|
||||
- common runtime delta: `AVAILABLE_NOT_COMPOSED`
|
||||
- 재검토:
|
||||
첫 제품 stream/Web Push를 선택할 때, 또는 backend replay/hosting/provider
|
||||
protocol이 바뀔 때
|
||||
|
||||
## 배경
|
||||
|
||||
현재 optional recipe catalog는 realtime capability에
|
||||
`referenceRuntime.status=AVAILABLE_NOT_COMPOSED`를 기록한다. 공통 event authority,
|
||||
bounded reconnect owner, single-writer live↔Poll handoff, fetch-stream SSE,
|
||||
bounded Polling, closed WebSocket protocol과 Web Push window/worker adapter는
|
||||
deterministic test와 함께 존재하지만 production entry에서는 제외된다. generic
|
||||
mega `RealtimePort`, 제품 event schema, 실제 endpoint, backend replay/provider
|
||||
contract와 composition은 선택하지 않았다.
|
||||
|
||||
추가 설계 범위에는 성격이 다른 네 capability가 있다.
|
||||
|
||||
- SSE: active document의 server-to-client event stream
|
||||
- WebSocket: active document의 duplex application protocol
|
||||
- Web Push: inactive browser에도 도착할 수 있는 Service Worker 기반 notification
|
||||
- bounded polling: 기존 HTTP/query operation의 제한된 scheduling policy
|
||||
|
||||
이를 “realtime transport” 하나로 합치면 다음 문제가 생긴다.
|
||||
|
||||
- Web Push의 permission, push service와 worker lifecycle이 connection 상태에 숨는다.
|
||||
- Polling을 무한 timer나 transport downgrade로 오해한다.
|
||||
- WebSocket이 필요하지 않은 server notification까지 duplex protocol이 된다.
|
||||
- connection open, event delivery, application effect와 server 최신성을 같은 성공으로
|
||||
표시한다.
|
||||
- auth refresh, reconnect, HTTP retry와 Query retry가 중첩된다.
|
||||
- gap, cursor expiry와 browser restore 뒤 authoritative resync owner가 사라진다.
|
||||
- push subscription endpoint/key나 cursor가 일반 application state와 telemetry에
|
||||
노출될 수 있다.
|
||||
|
||||
기존 recipe의 generic `channel: string`, `sequence: number`, 고정
|
||||
`resumeToken`, callback과 `heartbeat()`는 선택 시 복사해 좁힐 출발점이다.
|
||||
scope/epoch, closed event type, byte/queue limit, gap/reset, 진행되는 cursor,
|
||||
generation과 effect certainty가 없어 production wire authority로 사용할 수 없다.
|
||||
|
||||
## 현재 상태
|
||||
|
||||
| 항목 | 상태 | 설명 |
|
||||
| --- | --- | --- |
|
||||
| optional realtime catalog/recipe | `RECIPE_AVAILABLE` / product `NOT_SELECTED` | uncomposed reference runtime과 conformance script가 있음 |
|
||||
| common event/recovery/reconnect runtime | `AVAILABLE_NOT_COMPOSED` | scope/gap/barrier authority, finite reconnect owner와 exact close classification test가 있음 |
|
||||
| live↔Poll handoff coordinator | `AVAILABLE_NOT_COMPOSED` | monotonic generation, one effect writer와 bounded checkpoint/quiescence test가 있음 |
|
||||
| SSE runtime | `AVAILABLE_NOT_COMPOSED` | fetch-stream parser/adapter/reconnect test 있음; local server/browser evidence pending |
|
||||
| WebSocket runtime | `AVAILABLE_NOT_COMPOSED` | exact handshake/protocol/queue/recovery test 있음; load/browser evidence pending |
|
||||
| bounded polling coordinator | `AVAILABLE_NOT_COMPOSED` | finite single-flight visible/online lease와 deterministic budget test 있음 |
|
||||
| Web Push window/worker runtime | `AVAILABLE_NOT_COMPOSED` | subscription, registration/revoke, durable fence, strict inbound worker factory가 있음; provider/browser evidence pending |
|
||||
| exactly-once/global ordering | `PLATFORM_LIMITED` | 공통 browser delivery 목표로 보장하지 않음 |
|
||||
| always-on background connection/polling | `PLATFORM_LIMITED` | hidden/frozen/terminated document에서 보장하지 않음 |
|
||||
| timely cross-browser Web Push | `PLATFORM_LIMITED` | provider/browser/OS가 즉시 delivery를 보장하지 않음 |
|
||||
|
||||
reference source는 `AVAILABLE_NOT_COMPOSED`까지 승격됐다. 그러나 이 ADR과
|
||||
deterministic test만으로 `COMPOSED` 또는 `PRODUCTION_READY`로 올리지 않는다.
|
||||
제품 endpoint/registry와 backend/provider/target-browser evidence가 생긴 뒤
|
||||
선택 capability만 별도 승격한다.
|
||||
|
||||
## 결정
|
||||
|
||||
### 1. 네 capability를 분리한다
|
||||
|
||||
다음 의미를 고정한다.
|
||||
|
||||
| capability | 선택 의미 | 기본 fallback |
|
||||
| --- | --- | --- |
|
||||
| SSE | foreground one-way ordered hint stream | bounded polling 또는 stale UI |
|
||||
| WebSocket | foreground duplex interaction protocol | 의미가 축소되지 않으면 bounded polling, 아니면 disabled/stale UI |
|
||||
| Web Push | background user-visible notification hint | foreground inbox/focus refresh |
|
||||
| bounded polling | finite visible HTTP scheduling | manual refresh/explicit stale UI |
|
||||
|
||||
Web Push는 SSE/WebSocket의 fallback이 아니라 보완 capability다. Polling은
|
||||
WebSocket duplex 기능을 대신할 수 없다. SSE↔WebSocket 자동 downgrade도 하지
|
||||
않는다. 같은 사용자 의미를 보존하는 fallback만 registry에 명시한다.
|
||||
|
||||
추가 transport를 선택하기 전 기존 TanStack Query의 focus/reconnect refetch와
|
||||
manual refresh가 측정된 freshness 요구를 만족하는지 먼저 확인한다.
|
||||
|
||||
Connect/gRPC-Web server stream은 VD-29/VD-27의 operation-bound API protocol이고 GraphQL
|
||||
subscription은 현재 `NOT_SELECTED`다. GraphQL `@defer`/`@stream`은 finite
|
||||
incremental HTTP response이지 realtime subscription이 아니다. RPC adapter가
|
||||
protocol-specific terminal proof와 protobuf decode/schema/mapper를 끝낸
|
||||
runtime-wide notification branch에서만 공통
|
||||
scope/gap/resync coordinator를 재사용한다. frame/media/trailer, reconnect와
|
||||
operation deadline owner를 SSE/WebSocket adapter로 합치거나 protobuf message를
|
||||
`REALTIME_EVENT_V1` JSON으로 다시 감싸지 않는다. Polling의 개별 attempt는 VD-23의
|
||||
terminal·replay-safe REST `QUERY` execution contract를 재사용하되 transport/Query
|
||||
retry는 끄고, 이 결정은 attempt 사이 bounded lease만 소유한다.
|
||||
|
||||
### 2. source of truth는 서버다
|
||||
|
||||
SSE/WebSocket event의 기본 효과는 registered `QueryInvalidationTopic`과 authoritative
|
||||
HTTP refetch다. raw event payload를 domain entity나 Query cache의 authoritative
|
||||
state로 자동 승격하지 않는다.
|
||||
|
||||
authoritative delta 적용은 event type별 server revision, base revision, commit
|
||||
뒤 publication, idempotent reducer, gap/reset과 snapshot reconciliation이 모두
|
||||
승인된 경우에만 별도 선택한다.
|
||||
|
||||
Web Push payload는 작은 opaque notification hint다. Poll response는 해당 HTTP
|
||||
representation의 결과다. 어느 것도 authorization이나 exactly-once effect를
|
||||
증명하지 않는다.
|
||||
|
||||
### 3. outbound connection과 inbound event adapter를 분리한다
|
||||
|
||||
outbound가 소유한다.
|
||||
|
||||
- fixed endpoint와 credential 협력
|
||||
- connect/subscribe/resume/reconnect/close
|
||||
- selected WebSocket typed send
|
||||
- push subscription register/revoke
|
||||
- bounded poll scheduling/cancel
|
||||
|
||||
inbound가 소유한다.
|
||||
|
||||
- raw byte/frame hard cap
|
||||
- UTF-8/JSON/schema/version 검증
|
||||
- stream/event/scope/generation 확인
|
||||
- dedupe/order/gap
|
||||
- feature input 또는 query invalidation mapping
|
||||
- effect 뒤 cursor/ack commit
|
||||
|
||||
application/domain에 native browser, TanStack, URL/header나 vendor type을 노출하지
|
||||
않는다. `send(unknown)`과 arbitrary `channel`/endpoint도 금지한다.
|
||||
|
||||
### 4. target event protocol을 versioning한다
|
||||
|
||||
foreground common envelope은 다음 의미를 가져야 한다.
|
||||
|
||||
```text
|
||||
protocol = REALTIME_EVENT_V1
|
||||
streamId = registry-owned ID
|
||||
streamEpoch = opaque server reset epoch
|
||||
eventType = closed registry ID
|
||||
eventId = bounded dedupe ID
|
||||
sequence = canonical unsigned decimal string
|
||||
recoveryMode = CURSOR | SNAPSHOT_ONLY | SESSION_REBUILD
|
||||
resumeCursor = CURSOR면 opaque replay position, 아니면 exact null
|
||||
occurredAt = strict RFC 3339, ordering authority 아님
|
||||
scopeBinding = session/BFF-issued opaque exact-match token
|
||||
payload = event-type-specific closed codec
|
||||
```
|
||||
|
||||
`eventId`, `sequence`, `resumeCursor`와 business revision은 별도 의미다.
|
||||
sequence는 JSON safe-integer 문제를 피하도록 decimal string으로 전달하고
|
||||
stream + epoch 안에서만 비교한다.
|
||||
|
||||
credential, readable subject/account ID, signed URL, PushSubscription material과
|
||||
자유 형식 message는 envelope에 넣지 않는다.
|
||||
event type registry는 payload schema, pure boundary mapper와 effect profile을
|
||||
함께 bind한다. `scopeBinding`은 cache fingerprint/authorization proof가 아니고,
|
||||
cursor는 protocol/stream/feed/epoch/registered subscription set/auth scope에
|
||||
server-side로 bind한다. client는 opaque cursor를 해석하지 않는다.
|
||||
state-bearing stream의 recovery profile은 snapshot operation/checkpoint codec과
|
||||
replay/connect-buffer/server-hold barrier를 닫는다. `SESSION_REBUILD`는
|
||||
EPHEMERAL-only다. V1 server-side subset filter는 `NOT_SELECTED`이며 필요하면
|
||||
contiguous sequence/checkpoint를 가진 별도 stream으로 등록한다.
|
||||
|
||||
### 5. delivery guarantee와 authoritative resync를 분리한다
|
||||
|
||||
apply 순서는 다음과 같다.
|
||||
|
||||
```text
|
||||
byte cap
|
||||
-> parse/schema/version
|
||||
-> registry/scope/generation
|
||||
-> dedupe/order/gap
|
||||
-> registered boundary mapper
|
||||
-> sequential application effect
|
||||
-> effect commit
|
||||
-> last-applied cursor
|
||||
-> optional selected WebSocket protocol ACK
|
||||
```
|
||||
|
||||
effect 뒤 cursor를 commit하므로 crash window에서 duplicate가 생길 수 있다.
|
||||
effect는 idempotent하거나 query invalidation/refetch여야 한다.
|
||||
|
||||
- 전체 browser lifecycle에 대한 delivery guarantee는 없음
|
||||
- retention 안의 `CURSOR` foreground event 처리만 duplicate-tolerant
|
||||
at-least-once model
|
||||
- V1 ordering은 stream-wide 하나; partition은 별도 logical stream
|
||||
- exact duplicate/old sequence는 safe drop
|
||||
- 같은 event ID/sequence의 conflicting content는 protocol failure
|
||||
- old captured generation callback만 safe drop; current connection의
|
||||
`scopeBinding` mismatch는 security protocol violation으로 close/revalidate/resync
|
||||
- sequence gap, stream epoch change, cursor expiry, queue overflow는 delta 적용 중단
|
||||
- authoritative snapshot과
|
||||
`SnapshotCheckpoint(streamEpoch,lastAppliedSequence,resumeCursor|null,snapshotRevision)`를
|
||||
같은 commit point로 얻은 뒤에만 resume
|
||||
- exactly-once와 global ordering은 비목표
|
||||
|
||||
backend는 commit 이후 publication, replay retention, cursor reset과
|
||||
snapshot/checkpoint 의미를 소유한다. subscribe ACK는 accepted cursor와
|
||||
`nextExpectedSequence`를 반환한다. replay가 없는 `SNAPSHOT_ONLY`는
|
||||
connect/bounded-buffer 또는 server hold barrier 없이는 snapshot/connect 사이
|
||||
event를 잃을 수 있으므로 `CURRENT`를 보장하지 않고 finite revalidation/stale UX로
|
||||
degrade한다.
|
||||
|
||||
### 6. lifecycle은 scope generation으로 fence한다
|
||||
|
||||
connection, freshness, authorization, availability와 traffic admission을 별도
|
||||
상태 축으로 둔다. `connected: boolean` 하나로 표현하지 않는다.
|
||||
|
||||
- runtime config/release/session recovery 뒤에만 connect한다.
|
||||
- route lease는 unmount에서 release한다.
|
||||
- logout/account/release transition은 old generation을 먼저 fence한다.
|
||||
- connect/read/backoff/poll/snapshot을 abort하고 queue/cursor/dedupe를 폐기한다.
|
||||
- late event/response/worker handoff는 captured old generation이면 적용하지 않는다.
|
||||
- close/dispose/unsubscribe는 terminal/idempotent다.
|
||||
- React StrictMode 반복 뒤 physical listener/connection/timer가 하나만 남는다.
|
||||
- admission은 canonical `DISABLED | SHADOW | CANARY | ENABLED`만 사용하고,
|
||||
drain은 connection lifecycle의 `DRAINING`으로 표현한다.
|
||||
- `DISABLED`는 새 data-plane side effect를 0으로 한다. 이미 소유한 fixed
|
||||
resource의 idempotent close/revoke만 bounded `DRAINING` cleanup plane에서
|
||||
허용하며 `CLOSED` 뒤 network side effect는 0이다.
|
||||
|
||||
hidden에서는 Polling을 중지하고 live connection은 configured bounded grace 뒤
|
||||
close/pause한다. `pagehide`에서 document-owned SSE/WS/Poll을 모두 정리하고
|
||||
`pageshow`/visible 복귀에는 snapshot freshness gate 뒤 새 runtime으로 resume한다.
|
||||
`unload` 완료에 의존하지 않는다. backend는 active authorization revoke를
|
||||
close/control event로 전파하거나 bounded max connection age에 재인가한다.
|
||||
|
||||
### 7. retry owner를 하나로 제한한다
|
||||
|
||||
reconnect는 capped full-jitter exponential backoff를 사용한다. base/max delay,
|
||||
max attempts와 max elapsed는 immutable registry/implementation ceiling으로
|
||||
제한한다.
|
||||
|
||||
- stable-open window 또는 valid heartbeat/event 뒤에만 attempt reset
|
||||
- valid server hint는 local delay보다 이른 retry를 금지하는 not-before bound
|
||||
- server hint가 implementation max/remaining elapsed budget을 넘으면 낮춰
|
||||
clamp하지 않고 degraded/stale로 종료
|
||||
- offline에서는 timer retry를 멈춤
|
||||
- auth expiry는 session owner single-flight recovery 한 번
|
||||
- forbidden/protocol/schema failure는 terminal
|
||||
- 외부 rate/provider failure는 exact bounded server not-before hint가 있을 때만
|
||||
retry하고, hint가 없으면 terminal
|
||||
- retry budget 소진 뒤 declared Polling fallback 또는 stale UI
|
||||
- reconnect는 realtime coordinator, auth는 session owner, Poll cadence는 poll
|
||||
coordinator가 소유하고 Poll-bound HTTP/Query retry는 비활성
|
||||
- recovery checkpoint는 exact branded object identity로 다음 attempt에 전달한다.
|
||||
SSE `onOpen`/WebSocket `onSubscribed` proof와 attempt 성공 proof가 같은
|
||||
object일 때만 common transport barrier를 확인하고 event admission을 연다.
|
||||
clone/missing proof와 30초 readiness deadline 초과는 fail-closed다.
|
||||
- aborted sleep/attempt/closed-receipt는 기본 2초 bounded drain 뒤 run을
|
||||
fail-closed로 끝내되, 실제 old task가 settle할 때까지 `DRAINING`을 유지한다.
|
||||
정상 active session의 `waitClosed`에는 deadline을 두지 않는다.
|
||||
|
||||
### 8. SSE baseline은 bounded fetch-stream이다
|
||||
|
||||
common reference target은 fixed same-origin BFF에 대한 fetch-stream SSE다.
|
||||
native EventSource보다 다음을 명시적으로 제어하기 위해서다.
|
||||
|
||||
- credential integration
|
||||
- status/content type/redirect
|
||||
- AbortSignal과 lifecycle
|
||||
- parser/event byte ceiling
|
||||
- reconnect/idle/retry budget
|
||||
- explicit current cursor
|
||||
|
||||
native EventSource는 same-origin cookie auth, native `Last-Event-ID`/reconnect,
|
||||
`204` terminal contract와 lifecycle 뒤 cursor recovery를 backend가 수용한
|
||||
별도 profile에서만 허용한다. UA cursor를 application effect commit과 묶을 수
|
||||
없으므로 `INVALIDATION_HINT` 전용이고 reconnect/restore마다 authoritative
|
||||
snapshot gate를 수행한다. gate 중 hint는 bounded `pendingInvalidation`으로
|
||||
coalesce하고 checkpoint 뒤 pending refetch까지 drain한다. 이 buffer/barrier가
|
||||
없으면 `CURRENT`를 금지한다. `AUTHORITATIVE_DELTA`는 fetch-stream만 허용한다.
|
||||
token을 URL에 넣지 않는다.
|
||||
|
||||
fetch-stream parser는 표준 UTF-8 SSE format, BOM/line ending/comment/multi-line
|
||||
data/id/retry/incomplete EOF를 bounded하게 구현한다. exact `200
|
||||
text/event-stream`만 stream 성공이며 auth/rate/reset/provider status를 closed
|
||||
failure로 mapping한다. parsed candidate ID와 effect-committed cursor를 분리하고
|
||||
각 application event block의 직접 `id`와 envelope cursor를 exact match한다.
|
||||
|
||||
SSE baseline은 registry-owned session feed 하나와 feed-wide cursor 하나다.
|
||||
route lease는 local dispatch만 바꾸며 arbitrary server multiplex와
|
||||
per-subscription cursor는 `NOT_SELECTED`다.
|
||||
|
||||
hosting은 proxy buffering, idle/request timeout, heartbeat, cache/transform,
|
||||
HTTP connection budget와 client disconnect cleanup을 실제로 검증한다.
|
||||
|
||||
### 9. WebSocket은 versioned duplex protocol로만 선택한다
|
||||
|
||||
- fixed same-origin `wss:` endpoint와 exact subprotocol
|
||||
- server `Origin` 검증과 current session authorization
|
||||
- URL/query/subprotocol에 credential 금지
|
||||
- closed welcome/subscribe/unsubscribe-ack/event/reset/heartbeat/close frame
|
||||
- baseline text JSON, binary/extension은 별도 승인
|
||||
- application heartbeat/watchdog
|
||||
- bounded incoming sequential queue
|
||||
- bounded outgoing queue와 `bufferedAmount`
|
||||
- raw close reason redaction
|
||||
- same-epoch cursor resume의 `nextExpectedSequence = lastApplied + 1`; accepted
|
||||
cursor silent advance 금지, mismatch는 reset/snapshot
|
||||
- state-bearing initial subscribe는 snapshot/checkpoint + barrier 전 `CURRENT` 금지
|
||||
- `UNSUBSCRIBE` 뒤 matching `UNSUBSCRIBED`까지 tombstone과 quota를 유지하고 late
|
||||
event/control은 effect 없이 버린다. unknown ACK와 ACK deadline 초과는
|
||||
connection-level failure다.
|
||||
|
||||
classic browser WebSocket은 incoming backpressure를 제공하지 않으므로 queue
|
||||
overflow에서 임의 delta drop을 하지 않는다. baseline은 connection을 close하고
|
||||
snapshot resync한다. server의 bounded pause/resume ACK protocol을 별도 증명한
|
||||
profile에서만 subscription pause를 허용한다.
|
||||
|
||||
모든 client control frame은 하나의 FIFO outbound queue를 통과한다. negotiated
|
||||
message count/queued bytes와 native `bufferedAmount` 중 하나라도 넘으면
|
||||
`QUEUE_OVERFLOW`, `retryable=false`, `OVERLOADED`로 generation 전체를 닫고
|
||||
snapshot recovery를 요청한다.
|
||||
|
||||
durable business command는 기존 HTTP path를 기본으로 유지한다. WebSocket
|
||||
command를 선택하면 closed operation, command ID/idempotency, expected revision,
|
||||
ack와 business commit certainty를 별도로 정의한다.
|
||||
|
||||
### 10. Web Push는 별도 window/worker/backend capability다
|
||||
|
||||
Web Push 선택에는 다음이 모두 필요하다.
|
||||
|
||||
- user-action 기반 permission UX
|
||||
- active Service Worker registration
|
||||
- `userVisibleOnly: true`인 window subscription manager
|
||||
- authenticated backend register/revoke
|
||||
- server subscription registry
|
||||
- VAPID private-key/provider owner
|
||||
- worker push/notification/click inbound adapters
|
||||
|
||||
PushSubscription endpoint, `p256dh`, `auth`는 capability material로 취급하고
|
||||
application state, browser storage, URL, BroadcastChannel과 telemetry에서
|
||||
금지한다. VAPID private key는 server-only다.
|
||||
|
||||
push payload는 versioned, association/release-bound, expiring opaque notification
|
||||
hint로 제한한다. 개인 내용은 foreground BFF가 current authorization으로
|
||||
조회한다. worker handler는 `waitUntil` 안에서 bounded validation과
|
||||
`showNotification`만 수행하며 long retry/sync/migration을 하지 않는다.
|
||||
decoded application hint는 3 KiB를 넘지 않으며 최상위 JSON member name 중복은
|
||||
last-wins로 해석하지 않고 거절한다. `issuedAt`의 client clock 대비 future
|
||||
skew는 최대 5분, `expiresAt - issuedAt` lifetime은 최대 24시간이다.
|
||||
window의 native permission/subscription operation은 30초, backend
|
||||
register/reconcile/revoke operation은 15초 안에 종료하며 제품 config는 이
|
||||
implementation ceiling을 높일 수 없다.
|
||||
`pushsubscriptionchange` window handoff도 worker lifecycle abort와 10초
|
||||
deadline을 사용하고, non-cooperative `matchAll()` 또는 동기 `waitUntil()` 예외
|
||||
뒤에는 늦은 `postMessage`를 허용하지 않는다.
|
||||
|
||||
notification copy와 click route는 closed registry를 사용한다. arbitrary backend
|
||||
text나 URL을 OS notification/openWindow에 전달하지 않는다.
|
||||
worker restart 뒤 click을 처리하도록 bounded non-sensitive
|
||||
`NotificationClickDataV1`만 `NotificationOptions.data`에 넣고 click 시
|
||||
codec/expiry/current association/release를 다시 검증한다. logout 때 owned
|
||||
notification은 bounded best-effort close하지만 OS 잔존 가능성 때문에 copy는
|
||||
항상 account-neutral이어야 한다.
|
||||
|
||||
worker는 window in-memory session을 authority로 사용할 수 없으므로 opaque
|
||||
`fenceGeneration`, `sessionBindingEpoch`, `releaseEpoch`과
|
||||
`UNASSOCIATED | ACTIVE | REVOKED` association discriminant를 가진
|
||||
adapter-owned IndexedDB `PUSH_CONTROL_V1` record를 사용한다.
|
||||
account ID, endpoint/key, credential과 notification content는 이 record에서
|
||||
금지한다. missing/corrupt/mismatch는 fail-closed한다. 동일 association epoch의
|
||||
`REVOKED`는 terminal tombstone이다. logout은 durable fence generation rotate와
|
||||
REVOKED를 먼저 commit한다. 새 `ACTIVE`는 distinct backend epoch와 captured/current
|
||||
fence generation, server session binding, prior record revision/epoch, release를
|
||||
한 IDB transaction에서 CAS해 stale-tab response를 거절한다. 첫 register 전
|
||||
`UNASSOCIATED` record도 같은 generation을 durable하게 보관하므로 logout과
|
||||
in-flight register response의 race를 association sentinel 없이 닫는다. client
|
||||
`updatedAt`은 ordering authority가 아니다.
|
||||
|
||||
logout은 old generation fence, durable local association `REVOKED` commit과
|
||||
backend account association revoke를 정상 security commit으로 사용한다. boot에서
|
||||
native subscription/local fence/server association을 reconcile하고, local commit
|
||||
실패나 ambiguous revoke는 `PUSH_UNAVAILABLE`로 내려 짧은 TTL, send-time auth와
|
||||
click-time 재인가에 의존한다. native unsubscribe/old notification close는
|
||||
best-effort지만 captured native subscription, exact association tag와 unchanged
|
||||
durable fence를 모두 다시 확인한 경우에만 수행한다. 새 association이 commit되면
|
||||
old cleanup은 건너뛴다. local fence 실패 뒤 current native subscription 조회나
|
||||
association wildcard cleanup은 금지한다.
|
||||
|
||||
control tombstone purge는 자동 revoke 단계가 아니다. 별도 maintenance owner만
|
||||
captured revision/authority/association epoch가 exact한 `REVOKED` record를
|
||||
repository CAS로 삭제할 수 있고, concurrent newer owner가 있으면
|
||||
`STALE_REVISION`으로 끝난다.
|
||||
|
||||
backend register/revoke는 VD-23의 fixed `COMMAND`로 등록하고 cookie session의
|
||||
exact CSRF를 검증한다. register는 keyed idempotency 또는 atomic installation
|
||||
upsert/receipt, revoke는 duplicate/`ALREADY_GONE` 성공 의미를 가져야 하며
|
||||
`associationEpoch`은 server commit 뒤에만 발급한다.
|
||||
|
||||
Service Worker를 우회해 UA가 직접 notification을 표시할 수 있는 declarative push
|
||||
message는 V1에서 `NOT_SELECTED`다. outbound `web_push: 8030` shape를 거절하고
|
||||
별도 ADR 전에는 encrypted `WEB_PUSH_HINT_V1`만 허용한다.
|
||||
|
||||
Service Worker를 선택해도 offline fetch, PWA shell cache나 background sync가
|
||||
자동 승인되지 않는다. 하나의 worker composition/update owner가 선택된 handler를
|
||||
조립한다.
|
||||
|
||||
### 11. Polling은 bounded lease다
|
||||
|
||||
허용 형태:
|
||||
|
||||
- visible query의 낮은 빈도 conditional freshness poll
|
||||
- 사용자 시작 async job의 terminal-state convergence poll
|
||||
|
||||
각 lease는 operation owner, minimum/success/max interval, max attempts,
|
||||
max elapsed, response byte cap, visible-only policy와 terminal states를 가진다.
|
||||
|
||||
- operation은 registered terminal·replay-safe REST `QUERY`여야 함
|
||||
- Poll `maxAttempts`는 physical request 하나인 logical completion을 셈
|
||||
- Poll-bound VD-23 budget은 `maxAttempts=1`, `authRecoveryCount=0`,
|
||||
`maxCumulativeSleepMs=0`; TanStack Query retry도 끔
|
||||
- completion-chained timeout으로 single-flight
|
||||
- hidden/offline/pagehide/unmount/scope change/user cancel에서 stop
|
||||
- ETag/`If-None-Match` 또는 server cursor 사용
|
||||
- `304`, auth, cursor reset, `429/503 Retry-After`를 closed mapping
|
||||
- common recovery coordinator가 `POLL_ACTIVE -> LIVE_PROBING`에서 poll만 effect
|
||||
writer로 유지하고 live candidate는 bounded buffer만 사용. handoff mutex에서
|
||||
poll fence/abort + quiescence를 먼저 완료하고 current-generation
|
||||
snapshot/checkpoint와 buffered event를 적용한 뒤 live를 활성화
|
||||
- active writer effect tail도 in-flight 포함 256건/4MiB로 제한하고 overflow는
|
||||
전체 generation을 `QUEUE_OVERFLOW`로 fail-close
|
||||
- budget 소진 뒤 manual refresh/stale UI
|
||||
- page component `setInterval`과 unlimited loop 금지
|
||||
|
||||
### 12. resource ceiling과 privacy를 fail-closed한다
|
||||
|
||||
상세 설계의 target hard ceiling은 physical connection, logical subscription,
|
||||
event/frame/parser/queue/dedupe/reorder/outbound buffer, reconnect, poll lease,
|
||||
push hint와 worker deadline을 제한한다. 제품 config는 더 작게만 설정할 수 있다.
|
||||
|
||||
2026-07-28 reference-runtime amendment로, RT-01~RT-04 source 전체를
|
||||
tree-shaking 없이 합성하는 optional-recipe gzip 예산을 40,000 bytes로
|
||||
고정한다. 이는 production bundle 허용량이 아니며 미선택 production asset의
|
||||
realtime module 허용량은 계속 0이다. SSE replay-open과 WebSocket
|
||||
`SUBSCRIBED`가 exact recovery checkpoint를 증명하고 common barrier가 확인될
|
||||
때까지 event admission을 막는 readiness gate는 attempt당 최대 30초다.
|
||||
phase abort 뒤 비협조적인 retry sleep, connect attempt 또는 closed-receipt
|
||||
cleanup을 기다리는 drain은 2초로 고정하고 구현 절대 최대는 30초다. 상한을
|
||||
넘긴 task가 settle할 때까지 lifecycle은 `DRAINING`을 유지하며 정상 active
|
||||
session의 `waitClosed`에는 이 cleanup deadline을 적용하지 않는다.
|
||||
|
||||
ceiling 초과는 limit 자동 인상이나 silent drop이 아니라 new lease rejection,
|
||||
connection close, snapshot resync, typed backpressure, stale/degraded 또는
|
||||
notification drop으로 처리한다.
|
||||
|
||||
telemetry에는 transport/registry ID, closed outcome, count/duration/lag bucket만
|
||||
허용한다. raw URL/query/credential/subject/event ID/cursor/payload/close reason/
|
||||
PushSubscription key와 notification private content는 금지한다.
|
||||
|
||||
### 13. 실제 provider/browser/operations evidence 전에는 promotion하지 않는다
|
||||
|
||||
evidence를 분리한다.
|
||||
|
||||
1. pure unit/property와 deterministic fault contract
|
||||
2. 실제 local SSE/WS server integration
|
||||
3. backend replay/snapshot/auth/hosting/provider conformance
|
||||
4. built production asset의 target-browser lifecycle
|
||||
5. Web Push provider + browser/OS 자동·수동 evidence
|
||||
6. load/chaos/security negative gate
|
||||
7. dashboards, kill switch와 drain/recovery/rollback drill
|
||||
|
||||
fake/jsdom/MSW만으로 native stream, socket, worker, notification이나 provider
|
||||
readiness를 주장하지 않는다. 외부 evidence가 없으면 `PromotionEvidence`는
|
||||
`MISSING | PARTIAL`이고 promotion gate result는 `FAIL_UNVERIFIED`다.
|
||||
|
||||
## 선택하지 않은 대안
|
||||
|
||||
### 범용 transport enum을 가진 `RealtimePort`
|
||||
|
||||
전송 교체는 가능해 보이지만 direction, permission, lifecycle, delivery certainty와
|
||||
fallback 의미를 잃는다. 공통 protocol coordinator만 재사용하고 native capability
|
||||
port는 분리한다.
|
||||
|
||||
### 모든 server event에 WebSocket 사용
|
||||
|
||||
one-way notification에도 duplex handshake, heartbeat, queue와 server connection
|
||||
운영 비용을 강제한다. one-way stream은 SSE를 우선 검토한다.
|
||||
|
||||
### native EventSource만 공통 baseline으로 사용
|
||||
|
||||
arbitrary auth header, detailed status mapping, bounded reconnect와 explicit
|
||||
lifecycle cursor 제어가 부족하다. 조건부 profile로는 허용하지만 reference
|
||||
baseline은 fetch-stream이다.
|
||||
|
||||
### token을 SSE/WS URL에 전달
|
||||
|
||||
history, log, proxy, analytics와 referrer에 노출될 수 있다. same-origin
|
||||
BFF/cookie 또는 승인된 별도 handshake를 사용한다.
|
||||
|
||||
### event payload로 Query cache 직접 patch
|
||||
|
||||
filter/pagination/revision/gap 의미가 없으면 stale projection을 만든다. 기본은
|
||||
namespace invalidation과 authoritative refetch다.
|
||||
|
||||
### Web Push를 silent sync로 사용
|
||||
|
||||
permission/browser/OS/provider가 background execution과 timely delivery를
|
||||
보장하지 않는다. user-visible notification hint와 foreground refresh로 제한한다.
|
||||
|
||||
### 무한 `setInterval` Polling
|
||||
|
||||
overlap, hidden resource 사용, retry 중첩과 terminal cleanup 누락을 만든다.
|
||||
finite immutable lease와 single owner를 사용한다.
|
||||
|
||||
### cross-tab leader를 기본 제공
|
||||
|
||||
leader election/crash/handoff/partition과 SharedWorker 지원이 별도 protocol을
|
||||
요구한다. 기본은 tab별 bounded runtime과 focus snapshot이다.
|
||||
|
||||
### exactly-once delivery
|
||||
|
||||
cursor commit과 application effect 사이 crash window, push service와 browser
|
||||
lifecycle을 공통 frontend만으로 제거할 수 없다. retention 안의 CURSOR event만
|
||||
duplicate-tolerant하게 처리하고 나머지는 best-effort + authoritative resync를
|
||||
사용한다.
|
||||
|
||||
## 결과
|
||||
|
||||
긍정적 결과:
|
||||
|
||||
- 전송 선택이 요구와 failure semantics에 연결된다.
|
||||
- server state/query ownership과 clean architecture 경계를 유지한다.
|
||||
- gap, late callback, logout과 page restore가 명시적 복구 경로를 가진다.
|
||||
- Web Push permission/subscription material이 일반 realtime state와 분리된다.
|
||||
- Polling fallback이 resource-unbounded loop가 되지 않는다.
|
||||
- 미선택 capability의 bundle/worker/runtime side effect를 0으로 유지할 수 있다.
|
||||
|
||||
비용:
|
||||
|
||||
- common coordinator 외에도 transport별 adapter와 실제 provider harness가 필요하다.
|
||||
- backend는 replay/snapshot/outbox/auth와 provider 운영 계약을 제공해야 한다.
|
||||
- worker와 window에 별도 composition/test matrix가 필요하다.
|
||||
- direct delta보다 invalidation/refetch가 추가 HTTP 비용을 만들 수 있다.
|
||||
- target browser/OS에서 자동화할 수 없는 Web Push evidence를 운영해야 한다.
|
||||
|
||||
## 구현 순서
|
||||
|
||||
```text
|
||||
RT-00 contract/status
|
||||
-> RT-01 event authority + scope/gap/resync
|
||||
-> RT-02 SSE + bounded polling
|
||||
-> RT-03 WebSocket
|
||||
-> RT-04 Web Push
|
||||
-> RT-05 product composition/provider/browser/operations
|
||||
```
|
||||
|
||||
SSE와 WebSocket을 모두 구현해야 skeleton이 완성되는 것은 아니다. 공통
|
||||
mechanism을 구현한 뒤 실제 product requirement에 필요한 최소 transport만
|
||||
선택한다.
|
||||
|
||||
reference source와 deterministic/native evidence가 생기면 해당 runtime만
|
||||
`AVAILABLE_NOT_COMPOSED`로 올린다. 제품 endpoint/event registry/policy가
|
||||
bootstrap에 연결된 transport만 `COMPOSED`다.
|
||||
|
||||
## Rollout
|
||||
|
||||
capability별 traffic admission:
|
||||
|
||||
```text
|
||||
DISABLED -> SHADOW -> CANARY -> ENABLED
|
||||
SHADOW | CANARY | ENABLED -> DISABLED
|
||||
```
|
||||
|
||||
- transport, stream, Poll fallback과 push category kill switch를 분리한다.
|
||||
- safe config default는 `DISABLED`다.
|
||||
- canary 전에 backend/provider/browser/operations evidence를 만료 검증한다.
|
||||
- deploy/drain과 reconnect herd를 load test한다.
|
||||
- freshness/latency만 아니라 gap/resync/queue/memory/battery/push permission
|
||||
지표를 함께 본다.
|
||||
|
||||
## Rollback과 제거
|
||||
|
||||
1. admission을 `DISABLED`, connection lifecycle을 `DRAINING`으로 전환한다.
|
||||
2. logical subscription/send/poll/push registration을 중지한다.
|
||||
3. active reader/socket/timer/handler를 bounded close한다.
|
||||
4. HTTP focus/manual refresh 또는 명시된 fallback을 노출한다.
|
||||
5. server publisher/replay/subscription compatibility window를 유지한다.
|
||||
6. composition/registry/adapter/dependency/worker handler를 제거한다.
|
||||
7. CSP/runtime config/provider key와 retained server subscription을 정리한다.
|
||||
8. typecheck, architecture, tests, build, bundle/module inventory와 removal gate를
|
||||
실행한다.
|
||||
|
||||
미선택/제거 상태에서 connection, timer, push listener/subscription request와
|
||||
production bundle sentinel이 0이어야 한다.
|
||||
|
||||
## 완료 기준
|
||||
|
||||
### 이 결정의 설계 완료
|
||||
|
||||
- [x] 네 capability의 의미와 선택 조건을 분리했다.
|
||||
- [x] current status와 target runtime 상태를 구분했다.
|
||||
- [x] source of truth와 delivery/effect certainty를 정했다.
|
||||
- [x] target envelope, ordering, cursor와 resync를 정했다.
|
||||
- [x] lifecycle/retry/resource/security/privacy 경계를 정했다.
|
||||
- [x] transport별 auth/hosting/worker/Poll contract를 정했다.
|
||||
- [x] evidence, rollout, rollback과 제거 기준을 정했다.
|
||||
|
||||
### 구현과 promotion 상태
|
||||
|
||||
- [x] RT-01 공통 coordinator/reconnect/contract suite
|
||||
- [x] RT-02 SSE/Poll 및 single-writer handoff reference runtime과 deterministic evidence
|
||||
- [x] RT-03 WebSocket reference runtime과 deterministic evidence
|
||||
- [x] RT-04 Web Push window/worker reference runtime과 deterministic evidence
|
||||
- [x] static boundary/security fixture, synthetic bundle budget와 removal blocking gate
|
||||
- [ ] actual SSE/WS local server, load와 target-browser evidence
|
||||
- [ ] actual Web Push provider, permission UX와 target-browser evidence
|
||||
- [ ] provider/browser evidence와 operations drill의 release-blocking gate 등록
|
||||
- [ ] 실제 product/backend/provider selection
|
||||
- [ ] operations runbook drill
|
||||
|
||||
common runtime status는 `AVAILABLE_NOT_COMPOSED`다. 위의 미완료 promotion
|
||||
항목 전에는 product selection이 계속 `NOT_SELECTED`이고 production-ready를
|
||||
주장하지 않는다.
|
||||
|
||||
## 관련 자료
|
||||
|
||||
- [상세 설계](../realtime-events-web-push-and-bounded-polling.md)
|
||||
- [VD-10 optional capability recipes](./VD-10-optional-capability-recipes.md)
|
||||
- [VD-13 client cache scope and persistence](./VD-13-client-cache-scope-and-persistence.md)
|
||||
- [VD-23 API transport selection and REST execution](./VD-23-api-transport-selection-and-rest-execution.md)
|
||||
- [VD-25 Server State Cache lifecycle](./VD-25-server-state-cache-lifecycle.md)
|
||||
- [VD-27 gRPC-Web unary and server stream](./VD-27-grpc-web-unary-and-server-stream.md)
|
||||
- [API contract, Schema, Mapper와 Server State](../api-contract-schema-mapper-and-server-state.md)
|
||||
- [Optional adapter recipes](../optional-adapter-recipes.md)
|
||||
- [Client cache and browser storage](../client-cache-and-storage.md)
|
||||
- [Frontend ports, adapters, and boundaries](../frontend-ports-adapters-and-boundaries.md)
|
||||
- [WHATWG Server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html)
|
||||
- [WHATWG WebSockets](https://websockets.spec.whatwg.org/)
|
||||
- [W3C Push API](https://www.w3.org/TR/push-api/)
|
||||
- [WHATWG Notifications API](https://notifications.spec.whatwg.org/)
|
||||
- [W3C Service Workers](https://www.w3.org/TR/service-workers/)
|
||||
- [RFC 8030](https://www.rfc-editor.org/rfc/rfc8030)
|
||||
- [RFC 8291](https://www.rfc-editor.org/rfc/rfc8291)
|
||||
- [RFC 8292](https://www.rfc-editor.org/rfc/rfc8292)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user