270 lines
16 KiB
Markdown
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 수단으로 금지한다.
|