Files
tech-log-frontend/docs/architecture/decisions/VD-11-browser-file-and-origin-storage.md
T

270 lines
16 KiB
Markdown

# 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 수단으로 금지한다.