Files
clean-architecture-frontend…/docs/architecture/decisions/VD-14-resumable-download-and-background-transfer.md
T

899 lines
42 KiB
Markdown

# 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)