16 KiB
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를 암묵적으로 설치하므로 경계를 분리해야 한다.
결정
- 기존 동기식
StoragePort는 작은 public preference만 소유한다. IndexedDB, OPFS, Cache Storage를 backend enum 하나로 끼우지 않는다. - native
File,Blob,FileList,FileSystemHandle은 browser adapter의 transient vault 안에 둔다. application은 opaqueLocalFileRef, normalized metadata와 boundedreadRange()만 본다. - browser 파일 기능을 picker, file content, preview lease, download delivery로
분리한다. backend upload는
BrowserFileComposition의 구성요소가 아니며, 별도 선택 가능한ExampleQuarantinedUploadPort예시로만 둔다. user dismissal은 failure가 아닌 outcome이다. - backend upload를 선택한 경우 file validation, authorization, malware/archive/active-content 검사와 quarantine은 client hint보다 항상 authoritative하다.
- 큰 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하지 않는다. - download outcome은 browser handoff와 confirmed saved를 분리한다. anchor click을 disk write 완료로 기록하지 않는다.
- IndexedDB는 feature-specific async repository adapter다. raw database, transaction callback, store/index/schema version을 application에 노출하지 않는다.
- DB DDL version과 record codec version을 분리한다. schema upgrade는 짧고 additive하게, data migration은 resumable bounded batch로 수행한다.
- IndexedDB mutation은 request success가 아니라 transaction complete 이후에만 성공이다. revision CAS와 idempotency key를 기본 계약으로 둔다.
- 모든 connection은 versionchange/forced-close를 처리하고 blocked/future-schema 상태를 read-only 또는 online-only UX로 드러낸다. 자동 reload loop와 자동 database deletion을 금지한다.
- OPFS는 큰 immutable bytes와 integrity manifest만 소유한다. logical metadata, query, generation과 journal commit authority는 IndexedDB가 소유한다.
- IndexedDB와 OPFS 사이의 비원자성은
PREPARING -> FILES_READY -> COMMITTED -> CLEANEDjournal saga와 startup reconciliation으로 처리한다.COMMITTED만 사용자에게 보인다. - OPFS sync access handle은 DedicatedWorker의 신규 staging/chunk file에만
사용하고 항상 flush/close한다. committed file in-place overwrite와
readwrite-unsafe를 금지한다. - Cache Storage는 same-origin public GET representation 전용 platform-local facade다. auth, cookie-dependent, private, personal, no-store, opaque, 206, redirect response를 저장하지 않는다.
- Cache match는 query/Vary를 보존하고
ignoreSearch/ignoreVary를 금지한다. candidate 전체를 type/size/integrity 검증한 뒤에만 release를 활성화하며 verified previous release를 rollback용으로 유지한다. - Service Worker lifecycle과 Cache Storage ownership을 구분한다. unregister가 cache 삭제를 의미하지 않으므로 owned-prefix cleanup migration을 별도로 둔다.
- IndexedDB, OPFS와 Cache Storage는 origin quota budget을 공유한다.
estimate()는 rough signal이고 실제QuotaExceededError를 authority로 둔다. - credential 저장을 금지한다. same-origin client encryption을 XSS authorization boundary로 간주하지 않는다.
- fake는 계약 검증용이고 native production evidence를 대체하지 않는다. Chromium/Firefox/WebKit, multi-page, crash/fault, migration/rollback과 quota drill을 설치 capability의 promotion gate로 둔다.
- 공통 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은 별도다. - API lifecycle, transaction, bounded-memory, integrity와 recovery mechanism은 공통 adapter가 소유한다. schema/codec/query, authority, classification, retention, quota priority와 cache/file allowlist는 dataset/use-case 정책으로 주입한다.
- composition은 dataset별 opaque scope와 전체 storage policy를 검증해 깊은 snapshot/freeze한다. 공통 runtime을 여러 dataset의 전역 mega-repository로 구성하지 않는다.
- 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한다. - IndexedDB는 codec
measureStoredBytes, retention sidecar, datasetusedBytes/receiptCountbudget을 mutation과 같은 transaction에서 갱신한다. TTL은 sweep 전에도 read/query에서 보이지 않으며UNTIL_SYNCED는 confirmed record만 삭제 가능하다. lifecycle deletion은 composition authority의 opaque short-lived proof가 매 invocation 필요하고 proof는 검증 후 폐기한다. - idempotency receipt retention은 최대 31일, receipt configured cap의 구현 절대 상한은 1,000,000개다. codec migration은 old-writer drain proof와 revision fence가 필요하고 한 invocation은 최대 500 rows/30,000ms다.
- 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를 검증한다. - Cache manifest는 정규화된
expectedContentType까지 digest에 binding한다. response의 정규화된 Content-Type이 정확히 일치하지 않으면 candidate activation을 금지한다. - optional 상태는 metadata만으로 주장하지 않는다. real-browser JUnit verifier, Vite source-module inventory, source boundary gate와 runtime removal gate를 promotion evidence로 둔다.
- 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한다. BROWSER_MANAGED_RESOURCEdownload는 resource와 함께 서버 발급 capability receipt를 요구한다. synchronous resolver의 결과가 receipt/resource/media type/safe extension/server max/optional digest/expiry를 정확히 binding하지 않으면 handoff하지 않는다. strategy와 integrity mode를 caller가 선택하지 않는다.- 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한다. - OPFS의
LOGOUT,UNTIL_SYNCED,ACCOUNT_DELETIONpolicy maintenance는 composition이requestMaintenanceAuthorityprovider와consumeMaintenanceAuthorityconsumer를 모두 공급해야 한다. provider는 exact reason/frozen scope/frozen policy에 묶인 최대 5분 proof를 매번 새로 발급하고, consumer는 같은 binding과 expiry를 확인해 원자적으로 consume하여 replay를 막는다. application caller는 proof를 전달할 수 없고 runtime은 이를 저장·반환·관측하지 않는다. - origin-wide pressure/write admission/GC, OPFS·Cache forward migration, OPFS real preflight, bounded Cache maintenance와 preview decode safety의 후속 계약은 VD-15가 소유한다. 기존 store별 primitive를 그 coordinator의 구현 증거로 사용하지 않는다.
- Service Worker lifecycle, directory/persistent handle과 private/sparse Range
cache는 제품 선택 전
NOT_SELECTED인 별도 capability다. Range resumable download는 VD-14의DESIGNED_NOT_IMPLEMENTEDcapability이며 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로 올리지 않는다.
선택 이후 필요한 구현
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은 다음 순서를 따른다.
- 신규 write, worker activation과 cache candidate를 중지한다. 별도 upload workflow를 설치했다면 그 session도 독립적으로 중지한다.
- file ref/object URL/handle/connection/channel을 정리한다.
- offline read-write를 read-only 또는 online-only로 전환한다.
- N-1 bundle이 future schema를 destructive open 없이 감지하는지 확인한다.
- user-authored/unsynced data는 export/sync 확인 없이 purge하지 않는다.
- owned OPFS/cache namespace만 journal/manifest 기준으로 정리한다.
- adapter composition, runtime config와 dependency를 제거한다.
schema downgrade, blanket deleteDatabase(), caches.keys() 전체 삭제와 사용자
filename 기반 OPFS 삭제는 rollback 수단으로 금지한다.