feat: 기능 추가 과정중
This commit is contained in:
@@ -0,0 +1,965 @@
|
||||
# API contract, Schema, Mapper와 Server State platform
|
||||
|
||||
- 상태: capability별 current/target 분리, production design accepted
|
||||
- 기준일: 2026-07-28
|
||||
- 범위: REST, GraphQL over HTTP, Connect-Web/Connect, gRPC-Web,
|
||||
Protobuf/REST Gateway, runtime Schema, boundary Mapper, TanStack Query 기반
|
||||
Server State Cache
|
||||
- 관련 결정:
|
||||
- [VD-23 API transport selection과 REST execution](./decisions/VD-23-api-transport-selection-and-rest-execution.md)
|
||||
- [VD-24 Runtime schema와 boundary mapper](./decisions/VD-24-runtime-schema-and-boundary-mapper.md)
|
||||
- [VD-25 Server state cache lifecycle](./decisions/VD-25-server-state-cache-lifecycle.md)
|
||||
- [VD-26 Persisted GraphQL operation](./decisions/VD-26-persisted-graphql-operation.md)
|
||||
- [VD-27 gRPC-Web unary와 server stream](./decisions/VD-27-grpc-web-unary-and-server-stream.md)
|
||||
- [VD-29 Connect-Web와 browser Protobuf runtime](./decisions/VD-29-connect-web-and-browser-protobuf-runtime.md)
|
||||
- [VD-30 Protobuf contract와 REST Gateway](./decisions/VD-30-protobuf-contract-and-rest-gateway.md)
|
||||
- browser Protobuf/gateway 상세 설계:
|
||||
[Protobuf browser transport와 REST Gateway](./protobuf-browser-transport-and-rest-gateway.md)
|
||||
- backend handoff:
|
||||
[Backend API와 Server State contract](./backend-api-and-server-state-contract.md)
|
||||
- 운영 절차:
|
||||
[API contract와 server-state recovery](../operations/api-contract-and-server-state-recovery.md)
|
||||
|
||||
## 1. 목적
|
||||
|
||||
이 문서는 다음 질문을 하나의 production 계약으로 닫는다.
|
||||
|
||||
- REST, GraphQL, Connect와 gRPC-Web 중 무엇을 어디에 사용하는가
|
||||
- Protobuf contract와 REST Gateway가 transport/runtime과 어떻게 분리되는가
|
||||
- request/response가 어느 지점까지 untrusted wire data인가
|
||||
- TypeScript type, generated code와 runtime validation의 역할은 무엇인가
|
||||
- DTO를 domain/application projection으로 누가 변환하는가
|
||||
- server response를 어떤 query identity와 lifecycle로 cache하는가
|
||||
- schema, mapper, transport와 cache가 바뀔 때 어떻게 배포·관측·rollback하는가
|
||||
|
||||
각 protocol은 서로 대체 가능한 URL 호출 문법이 아니다. transport-specific
|
||||
codec, proxy와 failure semantics는 adapter가 소유한다. application은 transport
|
||||
종류, URL, GraphQL document, protobuf message나 TanStack Query를 직접 알지 않고
|
||||
feature-owned gateway와 application input만 호출한다.
|
||||
|
||||
## 2. 상태 모델
|
||||
|
||||
이 문서는 browser data 설계와 동일한 primary current-status literal을 사용한다.
|
||||
|
||||
| primary status | 의미 |
|
||||
| --- | --- |
|
||||
| `COMPOSED` | production bootstrap 또는 설치된 feature 호출 경로에 concrete runtime이 실제 연결돼 있다. |
|
||||
| `AVAILABLE_NOT_COMPOSED` | 실행 가능한 reference runtime과 test가 있지만 production graph에는 연결하지 않았다. |
|
||||
| `DESIGNED_NOT_IMPLEMENTED` | 계약·불변조건·failure와 promotion 기준은 승인됐지만 해당 runtime 또는 필수 orchestration이 없다. |
|
||||
| `NOT_SELECTED` | 제품 요구, owner와 비용이 승인되지 않아 의도적으로 선택하지 않았다. |
|
||||
| `PLATFORM_LIMITED` | target browser/protocol이 요구 semantics를 공통으로 보장하지 못한다. |
|
||||
|
||||
primary status와 다음 readiness 축을 섞지 않는다.
|
||||
|
||||
```text
|
||||
Selection
|
||||
NOT_SELECTED | SELECTED | REMOVING
|
||||
|
||||
TrafficAdmission
|
||||
DISABLED | SHADOW | CANARY | ENABLED
|
||||
|
||||
RuntimeHealth
|
||||
UNKNOWN | AVAILABLE | DEGRADED | UNAVAILABLE | INCOMPATIBLE
|
||||
|
||||
PromotionEvidence
|
||||
MISSING | PARTIAL | COMPLETE | EXPIRED
|
||||
```
|
||||
|
||||
`COMPOSED`는 traffic이 켜졌거나 provider가 conformant라는 뜻이 아니다.
|
||||
`AVAILABLE_NOT_COMPOSED`도 제품 bundle에 dependency가 들어갔다는 뜻이 아니다.
|
||||
|
||||
## 3. 현재 capability ledger
|
||||
|
||||
| capability | primary current status | 현재 증거 | 목표 또는 잔여 |
|
||||
| --- | --- | --- | --- |
|
||||
| installed REST reference vertical | `COMPOSED` | operation registry → request schema → HTTP → envelope/payload schema → mapper → application input → Query 화면 경로 | 아래 REST hardening delta와 실제 제품 provider 계약 |
|
||||
| shared REST JSON executor | `COMPOSED` | path/search/body codec projection, shared deadline/retry-sleep budget, AbortSignal, bounded auth recovery, exact envelope/media/status, safe failure와 diagnostics | 204/304/412 execution join과 actual provider conformance |
|
||||
| REST v2 security/execution baseline | `COMPOSED` | collision-aware operation composition, path placeholder↔codec key exact join, prefix-preserving HTTPS/loopback provider, named bearer/CSRF profile와 credential-mode ceiling, auth fail-before-fetch, bounded JSON, outbound correlation/status·physical-attempt 관측 | cookie-CSRF/CORS provider evidence, 204/304/412 conditional execution과 compatibility artifact |
|
||||
| GraphQL provider-neutral reference adapter | `DESIGNED_NOT_IMPLEMENTED` | source/dependency/codegen/runtime 없음 | persisted-operation-only transport, GraphQL response decoder, mapper binding과 contract harness |
|
||||
| product GraphQL composition | `NOT_SELECTED` | endpoint/schema/persisted manifest/owner 없음 | 제품 query가 REST보다 GraphQL aggregation을 정당화할 때 선택 |
|
||||
| GraphQL batching, subscription, `@defer`/`@stream` | `NOT_SELECTED` | 없음 | 각각 독립 ADR, proxy/browser lifecycle과 cache semantics 필요 |
|
||||
| provider-neutral Browser RPC V3 contract/runtime | `AVAILABLE_NOT_COMPOSED` | operation/profile/schema/mapper/encoder/transport exact join, typed application port, bounded unary retry/deadline/abort, server-stream idle/total/message/terminal/generation fence와 fail-closed unavailable adapter test | selected descriptor/generated client와 protocol-specific bounded transport를 붙이고 actual provider/browser conformance |
|
||||
| gRPC-Web unary reference adapter | `DESIGNED_NOT_IMPLEMENTED` | source/dependency/generated message 없음 | fixed method registry, protobuf codec, trailers/status/deadline와 proxy conformance |
|
||||
| gRPC-Web server-stream reference adapter | `DESIGNED_NOT_IMPLEMENTED` | source 없음 | bounded frame/idle/total budget, sequence/resume application protocol과 stream port |
|
||||
| product gRPC-Web composition | `NOT_SELECTED` | service descriptor/proxy/owner 없음 | browser-facing gRPC-Web gateway가 실제 이점을 줄 때 선택 |
|
||||
| gRPC-Web client-streaming/bidi guarantee | `PLATFORM_LIMITED` | gRPC-Web browser baseline이 해당 semantics를 제공하지 않음 | REST upload, WebSocket/WebTransport 또는 별도 protocol을 선택 |
|
||||
| Protobuf schema/codegen governance | `DESIGNED_NOT_IMPLEMENTED` | `.proto`, Buf config, descriptor와 generated output 없음 | authenticated source, immutable descriptor, deterministic codegen과 compatibility evidence |
|
||||
| Connect-Web unary/server-stream reference adapter | `DESIGNED_NOT_IMPLEMENTED` | `@connectrpc/*`, `@bufbuild/protobuf`, generated service와 provider 없음 | exact Connect/gRPC-Web transport row, bounded decode와 actual browser/provider conformance |
|
||||
| product Connect protocol composition | `NOT_SELECTED` | service/provider/owner 없음 | Protobuf-first backend의 selected browser operation이 있을 때만 선택 |
|
||||
| Connect browser client-streaming/bidi guarantee | `PLATFORM_LIMITED` | Connect protocol 기능과 browser request-stream 지원은 다름 | 별도 duplex/application protocol 선택 |
|
||||
| Protobuf REST Gateway reference contract/harness | `DESIGNED_NOT_IMPLEMENTED` | HttpRule/transcoder/OpenAPI/provider fixture 없음 | selected kind의 deterministic/provider conformance |
|
||||
| product Protobuf REST Gateway composition | `NOT_SELECTED` | route/provider/owner 없음 | curated BFF, grpc-gateway 또는 Envoy transcoder 중 하나와 public HTTP contract 승인 |
|
||||
| feature runtime request/response Schema | `COMPOSED` | Zod request/payload schemas와 installed schema registry가 reference feature에 연결 | byte/depth/node ceiling, unknown-field profile, schema artifact/digest와 multi-protocol source governance |
|
||||
| schema/mapper v2 reference baseline | `COMPOSED` | collision-aware schema codec/mapper contribution install, operation schema/mapper reference resolution, request reject/response strip 방향, bounded collection, typed no-throw mapping result와 operation별 cast-free result guard | actual codec fingerprint, source provenance와 multi-protocol compatibility policy |
|
||||
| generated contract artifact governance | `DESIGNED_NOT_IMPLEMENTED` | `generated-api` recipe만 있고 generator/provider 선택 없음 | OpenAPI/GraphQL/proto source authentication, pinned generation, drift/breaking gate와 N/N-1 |
|
||||
| feature boundary Mapper | `COMPOSED` | installed mapper metadata composer와 response-schema exact join 뒤 typed no-throw MappingResult → immutable domain/application view 실행 | numeric/date/null/enum canonical rules와 generated-artifact join |
|
||||
| TanStack Query memory Server State | `COMPOSED` | QueryClient, cancellation, stale-degraded UI, session-generation cancel/clear fence와 invalidation coordinator | account identity projection과 bounded topic↔namespace many-to-many registry |
|
||||
| reference bound-query/server-state profile | `COMPOSED` | bound definition, strict canonical input, scope-private opaque identity, active lease/LRU/collision/entry-byte ceiling, per-profile policy와 result admission | account projection, conditional HTTP execution join과 pagination composition |
|
||||
| mutation duplicate coordinator baseline | `COMPOSED` | QueryClient runtime/scope 단위 exact semantic input identity로 identical만 join하고 distinct input을 합치지 않으며 late scope result를 폐기 | logical-key serialization과 effect certainty/reconcile |
|
||||
| optimistic ordered-layer runtime | `AVAILABLE_NOT_COMPOSED` | out-of-order commit/rollback, authoritative external update 재적용과 expired-scope 제거 test | 제품 mutation의 deterministic membership/revision contract 승인 뒤 definition에 연결 |
|
||||
| conditional validator CAS sidecar | `AVAILABLE_NOT_COMPOSED` | scope/representation/cache revision exact binding, ETag validation과 bounded capacity test; session 전환 clear는 production infrastructure에 연결 | HTTP If-None-Match/304 query transaction과 query removal lifecycle join |
|
||||
| bounded cursor chain runtime | `AVAILABLE_NOT_COMPOSED` | page invariant, cursor loop, snapshot drift, page/item/byte/cursor ceiling과 abort test | backend CursorPage DTO/next cursor 계약 후 reference/infinite-query binding |
|
||||
| cross-context server-state invalidation | `COMPOSED` | singular opaque topic 기반 invalidate-only coordinator와 session-generation local reset | account projection과 bounded topic↔namespace many-to-many registry |
|
||||
| normalized GraphQL entity cache | `NOT_SELECTED` | 없음 | TanStack operation-result cache로 해결되지 않는 측정된 요구가 있을 때 별도 선택 |
|
||||
| persisted query cache | reference `DESIGNED_NOT_IMPLEMENTED`, product `NOT_SELECTED` | Web Storage persistence는 금지, IndexedDB persister 없음 | VD-13의 scope/retention/restore gate를 별도 통과 |
|
||||
| offline mutation queue | `NOT_SELECTED` | foreground mutation만 존재 | backend idempotency/cursor/conflict protocol과 durable command owner 필요 |
|
||||
|
||||
현재 REST reference는 `Response.json()`이 아니라 byte-bounded reader를 사용하고,
|
||||
external auth owner는 allowlisted header patch만 반환한다. 인증 통합 실패는 fetch
|
||||
전에 닫히며 correlation, success status와 physical attempt가 terminal observation에
|
||||
반영된다. 다만 이 baseline을 conditional response, complete pagination,
|
||||
provider conformance나 GraphQL/gRPC runtime의 증거로 재사용하지 않는다.
|
||||
마찬가지로 Browser RPC V3 공통 coordinator의 `AVAILABLE_NOT_COMPOSED` 판정은
|
||||
vendor wire adapter의 구현 판정이 아니다. `@connectrpc/*`, official grpc-web,
|
||||
generated message와 descriptor가 없는 현재 상태에서 Connect/gRPC-Web 각 row는
|
||||
계속 `DESIGNED_NOT_IMPLEMENTED`다.
|
||||
|
||||
## 4. 최상위 경계
|
||||
|
||||
```text
|
||||
presentation
|
||||
-> feature application input
|
||||
-> feature use case
|
||||
-> feature gateway port
|
||||
-> operation registry
|
||||
-> REST adapter
|
||||
-> GraphQL adapter
|
||||
-> Connect adapter
|
||||
-> gRPC-Web adapter
|
||||
-> bounded wire decoder
|
||||
-> runtime schema / semantic validation
|
||||
-> boundary mapper
|
||||
-> immutable application projection
|
||||
-> server-state query adapter
|
||||
-> registry-owned query identity and policy
|
||||
-> mapped application result only
|
||||
```
|
||||
|
||||
금지 경로:
|
||||
|
||||
```text
|
||||
page -> fetch / GraphQL SDK / generated Connect/gRPC client
|
||||
page -> raw URL / query document / protobuf message
|
||||
transport DTO -> domain or presentation public type
|
||||
Response / GraphQL response / generated message -> Query cache
|
||||
Query cache -> authorization or business conflict authority
|
||||
```
|
||||
|
||||
application port는 use-case 의미를 표현한다. 예를 들어
|
||||
`listResources(filters)`, `createResource(command)`는 허용하지만
|
||||
`executeGraphql(document, variables)`, `grpcCall(service, method, bytes)`와
|
||||
`request(url, options)`는 허용하지 않는다.
|
||||
|
||||
## 5. Protocol-neutral operation contract
|
||||
|
||||
각 외부 호출은 build-time registry의 discriminated row 하나로 고정한다.
|
||||
application caller는 `operationId`와 schema가 허용한 input만 제출한다.
|
||||
|
||||
```text
|
||||
ApiOperationContractV3
|
||||
registryVersion
|
||||
operationId
|
||||
owner
|
||||
protocol = REST | GRAPHQL_HTTP | CONNECT_HTTP | GRPC_WEB
|
||||
semantics = QUERY | COMMAND | SERVER_STREAM
|
||||
authProfileId
|
||||
csrfProfileId
|
||||
replayPolicy = SAFE | IDEMPOTENT | KEYED_COMMAND | NON_REPLAYABLE
|
||||
idempotencyKeyPolicy = NONE | REQUIRED
|
||||
requestSchemaId
|
||||
responseSchemaId
|
||||
mapperId
|
||||
errorProfileId
|
||||
deadlineProfileId
|
||||
retryProfileId
|
||||
serverStateProfileId | null
|
||||
invalidationTopicRefs[] = { topicId, topicVersion }
|
||||
dataClassification
|
||||
compatibility
|
||||
globalApiContractVersion
|
||||
protocolArtifactId
|
||||
minimumServerVersion
|
||||
retirementEpoch | null
|
||||
protocolBinding
|
||||
```
|
||||
|
||||
`protocolBinding`은 transport별 closed union이다.
|
||||
|
||||
```text
|
||||
REST
|
||||
method
|
||||
relativePathTemplate
|
||||
requestProjection
|
||||
requestMediaProfile
|
||||
responseMediaProfile
|
||||
conditionalProfile
|
||||
|
||||
GRAPHQL_HTTP
|
||||
endpointId
|
||||
graphqlHttpProfileRevision
|
||||
persistedEnvelopeProfileId
|
||||
responseStatusMediaProfileId
|
||||
persistedOperationId
|
||||
persistedOperationSha256
|
||||
operationType
|
||||
partialDataPolicy
|
||||
|
||||
GRPC_WEB
|
||||
endpointId
|
||||
clientRuntimeId
|
||||
grpcWebWireSpecRevision
|
||||
transportProfile
|
||||
responseHttpStatusProfileId
|
||||
fullyQualifiedService
|
||||
method
|
||||
rpcKind = UNARY | SERVER_STREAM
|
||||
requestMessageId
|
||||
responseMessageId
|
||||
|
||||
CONNECT_HTTP
|
||||
endpointId
|
||||
clientRuntimeId
|
||||
connectProtocolRevision
|
||||
encoding = PROTO_JSON | PROTO_BINARY
|
||||
requestMethod = POST | GET
|
||||
fullyQualifiedService
|
||||
method
|
||||
rpcKind = UNARY | SERVER_STREAM
|
||||
descriptorArtifactId
|
||||
descriptorDigest
|
||||
```
|
||||
|
||||
registry validation은 다음을 build/boot 전에 거절한다.
|
||||
|
||||
- 중복 operation/mapper/profile ID
|
||||
- protocol과 맞지 않는 binding field
|
||||
- 등록되지 않은 schema, mapper, auth, deadline, retry와 cache profile
|
||||
- `QUERY`인데 replay policy가 `SAFE | IDEMPOTENT`가 아님
|
||||
- `KEYED_COMMAND`인데 idempotency key policy가 `REQUIRED`가 아니거나 backend
|
||||
dedupe/reconcile profile이 없음
|
||||
- `NON_REPLAYABLE`인데 network/401 replay가 enabled
|
||||
- `COMMAND`인데 cache profile이 query data owner로 지정됨
|
||||
- `SERVER_STREAM`인데 ordinary query cache profile을 사용
|
||||
- opening retry가 enabled인데 replay policy가 `SAFE | IDEMPOTENT`가 아님
|
||||
- `KEYED_COMMAND | NON_REPLAYABLE` server stream인데 opening retry가 enabled거나
|
||||
explicit resume/reconcile/dedupe profile이 없음
|
||||
- `SERVER_STREAM`인데 `protocol=CONNECT_HTTP | GRPC_WEB`과
|
||||
`rpcKind=SERVER_STREAM` 조합이 아님. REST SSE와 GraphQL subscription을 이
|
||||
registry 의미로 암묵 등록하지 않음
|
||||
- unsafe REST method 또는 GraphQL mutation인데 CSRF/replay/key 결정이 없음
|
||||
- gRPC-Web client/bidi method
|
||||
- Connect browser client/bidi method 또는 descriptor의 `NO_SIDE_EFFECTS`가 없는
|
||||
Connect GET
|
||||
- absolute URL, runtime GraphQL document 또는 caller-provided service/method
|
||||
- implementation hard ceiling보다 큰 timeout, byte, frame, page와 retry 값
|
||||
- GraphQL operation↔provider의 HTTP revision/envelope/status-media profile 또는
|
||||
Connect/gRPC-Web operation↔provider의 runtime/wire/status/capability tuple
|
||||
mismatch
|
||||
|
||||
현재 installed reference operation은 REST v2 metadata를 사용한다. operation,
|
||||
runtime schema codec과 mapper contribution은 object spread가 아니라 각각의
|
||||
collision-aware composer로 설치되며 duplicate ID를 덮어쓰기 전에 거절한다.
|
||||
boot-time binding 검증은 path placeholder와 codec key, provider/auth/CSRF profile,
|
||||
path/request/response schema와 mapper input schema를 exact resolve한 뒤 immutable
|
||||
registry를 발행한다. GraphQL/Connect/gRPC-Web discriminant와 protocol-specific
|
||||
binding은 해당 reference adapter가 아직 없으므로 source에 구현됐다고 표현하지
|
||||
않는다.
|
||||
|
||||
현재 `API_CONTRACT_VERSION`은 runtime config와 release manifest의 문자열 일치
|
||||
gate다. 목표 contract set은 REST/OpenAPI artifact, GraphQL schema/persisted
|
||||
manifest, protobuf descriptor, runtime schema/mapper registry digest를 포함한
|
||||
bounded manifest를 만들고 global compatibility version과 함께 release tuple에
|
||||
binding한다. 문자열 일치만 actual backend compatibility 증거로 사용하지 않는다.
|
||||
|
||||
## 6. 공통 실행 lifecycle
|
||||
|
||||
```text
|
||||
lookup exact operation
|
||||
-> freeze session/account/runtime generation
|
||||
-> validate and canonicalize application input
|
||||
-> derive exact query/command identity
|
||||
-> allocate total operation deadline
|
||||
-> encode transport request from registry binding
|
||||
-> attach credential/CSRF through approved owner
|
||||
-> execute bounded attempt
|
||||
-> bounded response/frame decode
|
||||
-> validate transport envelope/status
|
||||
-> validate operation DTO/message semantics
|
||||
-> map to immutable application projection
|
||||
-> re-check scope/generation
|
||||
-> return Result
|
||||
-> query adapter may admit mapped value to memory cache
|
||||
```
|
||||
|
||||
현재 v2 auth owner는 `Request`를 반환하지 않고 transport가 만든 immutable
|
||||
request binding에 대해 allowlisted credential patch만 제공한다. 최소한
|
||||
transport는 attach 뒤에도 URL, origin, method, body digest, content headers,
|
||||
idempotency와 conditional binding이 바뀌지 않았음을 다시 검증한다.
|
||||
auth-required operation은 session state가 unauthenticated/integration-failed이거나
|
||||
credential attachment가 실패하면 **fetch 0회**로 닫는다. 동시 401 recovery는
|
||||
session owner의 single-flight 한 번만 공유하며 replay-safe operation만 동일
|
||||
logical deadline/idempotency binding으로 한 번 재실행한다.
|
||||
|
||||
모든 async boundary와 terminal cache write 전에 captured generation을 확인한다.
|
||||
logout/account switch 뒤 끝난 response, mapper와 stream frame은 old runtime
|
||||
결과로 폐기한다.
|
||||
|
||||
deadline은 attempt마다 새로 시작하지 않는다.
|
||||
|
||||
```text
|
||||
total budget
|
||||
= credential attach
|
||||
+ network attempts
|
||||
+ retry delay
|
||||
+ body/frame read
|
||||
+ schema validation
|
||||
+ mapper
|
||||
```
|
||||
|
||||
각 phase에 별도 하위 ceiling을 둘 수 있지만 전체 deadline을 늘릴 수 없다.
|
||||
caller abort, runtime teardown, timeout과 provider cancellation은 서로 다른 safe
|
||||
failure로 정규화한다.
|
||||
|
||||
## 7. Transport 선택 기준
|
||||
|
||||
| 요구 | 기본 선택 | 이유 |
|
||||
| --- | --- | --- |
|
||||
| resource/command, HTTP cache/conditional semantics, 파일 handoff | REST | Web/BFF·CDN·운영 도구와 자연스럽고 failure/status가 명확함 |
|
||||
| 여러 aggregate를 한 화면 shape로 읽고 client별 selection이 유의미 | persisted GraphQL query | allowlisted operation으로 over/under-fetch를 줄일 수 있음 |
|
||||
| Protobuf-first backend의 내부 web UI, unary 또는 bounded server stream | Connect-Web/Connect 우선 평가 | generated descriptor와 Fetch 기반 browser RPC를 재사용 |
|
||||
| 기존 gRPC-Web proxy/conformance 자산 | selected gRPC-Web runtime | runtime별 binary/text/stream capability를 exact profile로 고정 |
|
||||
| ProtoJSON/HttpRule 자체가 승인된 public HTTP contract | generated REST Gateway 검토 | envelope/status/cache/idempotency를 별도 증명 |
|
||||
| 현재 REST envelope·ETag·Range·제품 DTO가 중요 | curated REST BFF 유지 | generated transcoder가 제품 HTTP 의미를 자동 제공하지 않음 |
|
||||
| browser client/bidi streaming | Connect/gRPC-Web 사용 금지 | protocol 자체 기능과 browser 공통 지원을 혼동하지 않음 |
|
||||
| arbitrary ad-hoc query | GraphQL 사용 금지 | cost, authorization, cache identity와 operation governance를 우회 |
|
||||
| 단순 CRUD인데 GraphQL/gRPC dependency만 추가 | REST 유지 | 복잡도와 bundle/proxy 비용을 정당화하지 못함 |
|
||||
|
||||
한 feature가 여러 protocol을 사용할 수 있지만 한 `operationId`는 한 protocol에만
|
||||
binding한다. query read를 shadow 비교하는 경우에도 secondary 결과는 사용자와
|
||||
cache에 반영하지 않는다. command는 protocol 장애를 이유로 자동 failover/replay
|
||||
하지 않는다.
|
||||
|
||||
## 8. REST 설계 요약
|
||||
|
||||
REST 상세 결정은 VD-23이 소유한다. 공통 baseline은 다음과 같다.
|
||||
|
||||
- base origin과 relative path template은 composition/registry가 소유한다.
|
||||
- provider base URL은 HTTPS와 exact origin/path-prefix를 고정하고 userinfo,
|
||||
query와 fragment를 금지한다. path join은 선택한 base prefix를 보존하며
|
||||
leading slash가 prefix를 조용히 제거하지 않는다.
|
||||
- method는 closed union이며 path/search/header/body는 각 runtime schema를 지난다.
|
||||
- replay semantics는 `SAFE`, `IDEMPOTENT`, `KEYED_COMMAND`,
|
||||
`NON_REPLAYABLE`로 method와 교차 검증한다.
|
||||
- caller-provided URL, header, `credentials`, redirect와 cache option을 금지한다.
|
||||
- cookie session이면 unsafe method에 approved CSRF owner가 필요하다.
|
||||
- `SAFE | IDEMPOTENT | KEYED_COMMAND` 중 exact retry profile과 실제 provider
|
||||
replay evidence가 있는 operation만 network retry한다.
|
||||
- keyed retry와 401 recovery는 같은 logical idempotency key를 유지한다.
|
||||
- total deadline, attempts, backoff와 `Retry-After`는 implementation ceiling 안이다.
|
||||
- JSON/error body는 present/valid `Content-Length` advisory preflight와 actual
|
||||
decoded-byte bounded stream reader 뒤 parse한다. encoded transfer cap은
|
||||
BFF/proxy/CDN가 집행한다.
|
||||
- status, content type, response media profile과 envelope 조합을 exact하게 검증한다.
|
||||
- 204, 304, 412, 422와 problem/envelope profile은 operation이 명시한 경우만 허용한다.
|
||||
- strong ETag는 cache identity가 아니라 exact representation revalidation
|
||||
metadata다. validator를 diagnostics에 기록하지 않는다. `Last-Modified`는
|
||||
별도 weak/time validator profile이 승인되기 전 이번 target에 포함하지 않는다.
|
||||
- cursor는 opaque하며 filter/sort/scope와 binding한다. arbitrary URL을
|
||||
`next` link로 따라가지 않는다.
|
||||
- browser HTTP cache와 application ETag/TanStack revalidation owner 중 하나를
|
||||
operation별로 선택한다. 두 cache의 freshness를 서로 추측해 합치지 않는다.
|
||||
- request/response body, URL query, authorization, CSRF, idempotency key와 raw
|
||||
backend copy를 log/telemetry에 넣지 않는다.
|
||||
|
||||
## 9. GraphQL 설계 요약
|
||||
|
||||
GraphQL 상세 결정은 VD-26이 소유한다. reference target은
|
||||
**persisted operation only**다.
|
||||
|
||||
- production bundle은 arbitrary GraphQL document string을 runtime에 받지 않는다.
|
||||
- build artifact가 operation name, stable ID, SHA-256, variables/result schema,
|
||||
schema digest와 owner를 manifest로 만든다.
|
||||
- BFF/router는 allowlist에 없는 ID/hash와 cost/depth limit 초과를 거절한다.
|
||||
- endpoint는 fixed HTTPS registry ID이고 POST가 기본이다.
|
||||
- selected provider가 지원하는 GraphQL-over-HTTP revision과 persisted-envelope
|
||||
extension을 profile에 고정한다. ID/hash-only request를 generic 표준 envelope로
|
||||
가장하지 않는다.
|
||||
- `Accept: application/graphql-response+json`을 우선하고 final URL/media/body cap
|
||||
뒤에는 허용 HTTP status의 GraphQL envelope를 bounded decode한 다음 status/body
|
||||
matrix를 교차 검증한다. legacy `application/json`은 별도 profile이다.
|
||||
- public cacheable query의 GET은 별도 threat/cache review 뒤에만 허용한다.
|
||||
- variables는 request schema와 byte/depth/node ceiling을 통과한다.
|
||||
- HTTP status와 GraphQL `data/errors/extensions`를 두 단계로 검증한다.
|
||||
- default `partialDataPolicy=REJECT`; 승인 operation만 typed completeness metadata와
|
||||
함께 partial을 application으로 투영할 수 있다.
|
||||
- error message, path value와 arbitrary extensions를 노출하지 않고 registered
|
||||
safe code/category만 `AppFailure`로 mapping한다.
|
||||
- APQ miss에서 full document를 자동 전송하지 않는다. manifest/version mismatch로
|
||||
fail-closed하고 coherent frontend/router artifact를 복구한다.
|
||||
- batching은 auth, deadline, cancel, observation과 partial failure owner가
|
||||
별도 승인되기 전 `NOT_SELECTED`다.
|
||||
- subscription, `@defer`, `@stream`은 ordinary query adapter에 암묵적으로 넣지
|
||||
않는다.
|
||||
- Apollo/urql normalized cache는 기본 dependency가 아니다. mapped operation
|
||||
result의 memory owner는 TanStack Query다.
|
||||
|
||||
## 10. Browser Protobuf RPC와 REST Gateway 설계 요약
|
||||
|
||||
축과 선택 기준의 상세 계약은
|
||||
[Protobuf browser transport와 REST Gateway](./protobuf-browser-transport-and-rest-gateway.md)가
|
||||
소유한다. Protobuf는 IDL/serialization, Connect와 gRPC-Web은 browser wire
|
||||
protocol, Connect-Web/official grpc-web은 client runtime, REST Gateway는 HTTP
|
||||
노출 방식이다. 네 이름을 하나의 대안 목록이나 하나의 auto-negotiating
|
||||
executor로 합치지 않는다.
|
||||
|
||||
### 10.1 gRPC-Web
|
||||
|
||||
gRPC-Web 상세 결정은 VD-27이 소유한다. reference target은 unary와 bounded
|
||||
server-stream만 다룬다.
|
||||
|
||||
- checked-in/generated artifact는 pinned proto descriptor/module digest에 묶는다.
|
||||
- vendor generated client/message는 feature adapter 내부에만 존재한다.
|
||||
- fully-qualified service/method와 endpoint는 registry가 고정한다.
|
||||
- client runtime과 gRPC-Web wire-spec revision을 operation/provider에 고정한다.
|
||||
official grpc-web runtime 기준 binary profile은 unary에만 사용하고 server
|
||||
stream은 `grpcwebtext`에 binding한다.
|
||||
- Connect-Web의 `createGrpcWebTransport()`는 Fetch 기반 binary/JSON unary와
|
||||
server stream profile이며 `grpcwebtext` profile이 아니다. official XHR runtime의
|
||||
capability matrix를 이 runtime에 적용하지 않는다. platform-authored custom
|
||||
binary streaming도 세 browser와 actual proxy의 incremental evidence가 있는
|
||||
별도 runtime profile만 허용한다.
|
||||
- frame header, message length, compression flag, total bytes와 frame count를
|
||||
bounded decoder가 검증한다.
|
||||
- HTTP status와 terminal status source를 함께 검사한다. terminal status source는
|
||||
body trailer frame 또는 zero-body trailers-only response header 중 정확히
|
||||
하나이며 중복/충돌을 거절한다.
|
||||
- `grpc-message`, binary error details와 metadata는 allowlist projection 없이
|
||||
application에 반환하지 않는다.
|
||||
- deadline은 `grpc-timeout`과 local total deadline의 더 짧은 값이며
|
||||
AbortSignal이 fetch/stream reader를 cancel한다.
|
||||
- unary `SAFE | IDEMPOTENT | KEYED_COMMAND` 중 provider evidence가 있는
|
||||
operation만 retry한다. stream reconnect는 retry가 아니라
|
||||
server-owned sequence/resume-token을 가진 별도 application protocol이다.
|
||||
- idle deadline, total deadline, max frame/message/count/buffer를 모두 둔다.
|
||||
- client-streaming/bidi는 지원한다고 가장하지 않는다.
|
||||
- Envoy/BFF/Connect/gRPC-Web proxy의 CORS, exposed trailers, content type,
|
||||
auth와 maximum message 설정을 actual provider conformance로 검증한다.
|
||||
- int64/uint64는 JavaScript number로 변환하지 않고 safe integer 범위를
|
||||
증명하거나 decimal string/adapter-private bigint로 mapping한다.
|
||||
|
||||
### 10.2 Connect-Web과 Connect protocol
|
||||
|
||||
Connect 상세 결정은 VD-29가 소유한다.
|
||||
|
||||
- `createConnectTransport()`와 `createGrpcWebTransport()`는 같은 package의 서로
|
||||
다른 wire protocol이다. decoder/status/terminal profile을 공유하지 않는다.
|
||||
- Connect unary의 JSON/binary와 POST를 operation row에 고정한다.
|
||||
- GET은 unary + `NO_SIDE_EFFECTS` descriptor + non-sensitive bounded input +
|
||||
exact URL/cache/CORS profile에서만 별도 승인한다.
|
||||
- Connect server stream은 final EndStream envelope를 확인하기 전 성공이 아니다.
|
||||
- stock runtime의 whole-body decode와 streaming compression 한계를 exact package
|
||||
version evidence로 확인한다. proxy cap이나 custom bounded transport가 없으면
|
||||
production raw-byte ceiling을 완료로 표시하지 않는다.
|
||||
- interceptor의 resolved onion order, auth 이후 final invariant, total deadline,
|
||||
exactly-one retry owner와 cancel handle을 manifest에 고정한다.
|
||||
- `@connectrpc/connect-query`를 기본 도입하지 않는다. generated service/message는
|
||||
adapter-private이고 mapped application value만 기존 TanStack Query에 들어간다.
|
||||
|
||||
### 10.3 Protobuf contract와 REST Gateway
|
||||
|
||||
Protobuf/REST Gateway 상세 결정은 VD-30이 소유한다.
|
||||
|
||||
- authenticated proto/Buf source, descriptor, generator/runtime/plugin version과
|
||||
generated digest를 coherent release artifact로 고정한다.
|
||||
- JSON 노출은 최소 `WIRE_JSON` compatibility를 요구하고 canonical HttpRule
|
||||
route manifest와 generated OpenAPI를 별도 semantic diff한다.
|
||||
- current reference REST envelope에는 curated BFF를 유지한다.
|
||||
- direct grpc-gateway/Envoy transcoder는 ProtoJSON, method/path/query/body,
|
||||
status/error/CORS/cache contract가 그대로 제품 API로 승인된 unary operation에만
|
||||
적용한다.
|
||||
- gateway는 idempotency store, CursorPage snapshot, ETag/304/412, file Range나
|
||||
안전한 domain error vocabulary를 자동 제공하지 않는다.
|
||||
- REST server streaming은 generated gateway의 부수 동작으로 활성화하지 않고
|
||||
framing/terminal/cache를 소유하는 별도 ADR 없이는 `NOT_SELECTED`다.
|
||||
|
||||
## 11. Schema trust boundary
|
||||
|
||||
TypeScript type과 generated code는 compile-time convenience이지 runtime proof가
|
||||
아니다. trust transition은 다음 순서를 지킨다.
|
||||
|
||||
```text
|
||||
untrusted bytes/frames
|
||||
-> bounded transport decoder
|
||||
-> transport envelope/status proof
|
||||
-> operation DTO/message runtime or semantic proof
|
||||
-> ValidatedWireValue (adapter-private)
|
||||
-> boundary mapper
|
||||
-> immutable application projection
|
||||
```
|
||||
|
||||
schema profile은 최소 다음을 고정한다. browser가 직접 집행하는 decoded ceiling과
|
||||
provider/BFF/proxy가 집행하는 wire/encoded ceiling의 owner를 분리한다.
|
||||
|
||||
```text
|
||||
schemaId
|
||||
schemaVersion
|
||||
boundary
|
||||
protocol
|
||||
sourceArtifactId + sourceDigest
|
||||
unknownFieldPolicy
|
||||
providerMaxEncodedBytes
|
||||
maxDecodedBytes
|
||||
maxDepth
|
||||
maxNodes
|
||||
maxStringBytes
|
||||
maxCollectionItems
|
||||
compatibilityPolicy
|
||||
owner
|
||||
```
|
||||
|
||||
unknown-field 기본 정책:
|
||||
|
||||
| 경계 | 정책 |
|
||||
| --- | --- |
|
||||
| request, config, capability, control envelope | `REJECT_UNKNOWN` |
|
||||
| evolvable ordinary response DTO | `STRIP_UNKNOWN` 후 mapper에 전달 |
|
||||
| discriminant/security/authorization 의미를 가진 union | unknown variant 거절 |
|
||||
| unknown data 보존 | adapter 내부 forward proxy가 아닌 한 금지 |
|
||||
|
||||
현재 reference request/path DTO는 `.strict()`로 unknown field를 거절하고,
|
||||
ordinary response DTO는 `.strip()` projection으로 additive server field를
|
||||
cache/domain 경계 밖에 버린다. discriminant/security union을 포함한 다른
|
||||
operation은 각 compatibility profile에 따라 별도로 결정한다.
|
||||
|
||||
## 12. Mapper 경계
|
||||
|
||||
mapper는 transport가 아니라 feature contract가 소유한다.
|
||||
|
||||
```text
|
||||
MapperDefinition
|
||||
mapperId
|
||||
inputSchemaId
|
||||
outputContractId
|
||||
mapperVersion
|
||||
collectionPolicy
|
||||
temporalPolicy
|
||||
numericPolicy
|
||||
nullabilityPolicy
|
||||
owner
|
||||
```
|
||||
|
||||
mapper는 pure, deterministic, side-effect-free이며 다음 union을 반환한다.
|
||||
|
||||
```text
|
||||
MappingResult<T>
|
||||
= { ok: true, value: T }
|
||||
| { ok: false, error: MAPPING_CONTRACT_VIOLATION }
|
||||
```
|
||||
|
||||
현재 reference mapper는 예상 가능한 drift를 throw하지 않고 closed
|
||||
`MappingResult` failure로 반환한다. registry 실행 경계는 예상하지 못한 mapper
|
||||
throw도 fail-closed mapping failure로 바꾸며 raw DTO, value, path와 backend
|
||||
message를 버린다.
|
||||
|
||||
공통 scalar 규칙:
|
||||
|
||||
- opaque ID는 trim/재해석하지 않는 bounded branded string이다.
|
||||
- ISO timestamp는 offset/precision 정책을 검증한 뒤 application instant로
|
||||
변환한다. locale date string과 invalid date normalization을 금지한다.
|
||||
- `int64`, decimal money와 high-precision value는 JSON number로 받지 않는다.
|
||||
- `null`, absent와 empty string/list는 schema와 domain에서 별도 의미로 결정한다.
|
||||
- unknown enum은 domain이 explicit `UNKNOWN`을 소유한 경우만 mapping한다.
|
||||
- collection mapping은 count/byte ceiling 안에서 fail-fast하고 partial array를
|
||||
cache하지 않는다.
|
||||
- mapper는 network, clock, storage, QueryClient, locale formatter와 telemetry를
|
||||
호출하지 않는다.
|
||||
|
||||
## 13. Server State Cache 경계
|
||||
|
||||
TanStack Query는 transport response cache가 아니라 mapped application projection의
|
||||
memory lifecycle owner다.
|
||||
|
||||
cache에 허용:
|
||||
|
||||
- immutable plain application projection
|
||||
- registered query identity로 찾을 수 있는 bounded collection/page
|
||||
- UI가 stale/refresh 상태를 계산하는 library metadata
|
||||
|
||||
cache에 금지:
|
||||
|
||||
- `Response`, raw JSON/GraphQL envelope, generated protobuf message
|
||||
- auth/CSRF/idempotency token, request header와 arbitrary URL
|
||||
- raw ETag, trace/span, backend error/details
|
||||
- File/Blob/stream/native handle
|
||||
- domain service, class instance, function, Promise와 `AbortSignal`
|
||||
|
||||
query profile은 caller가 raw option을 전달하는 대신 registry에서 선택한다.
|
||||
|
||||
```text
|
||||
ServerStateProfileV1
|
||||
profileId
|
||||
queryKeyCodecId
|
||||
scopePersistencePolicyId
|
||||
staleTimeMs
|
||||
gcTimeMs
|
||||
refetchOnFocus
|
||||
refetchOnReconnect
|
||||
networkMode
|
||||
maxResultBytes
|
||||
maxCollectionItems
|
||||
paginationProfileId | null
|
||||
revalidationProfileId | null
|
||||
invalidationTopicRefs[] = { topicId, topicVersion }
|
||||
placeholderPolicy
|
||||
owner
|
||||
```
|
||||
|
||||
query key는 validated/canonical application input과 scope projection으로 만든다.
|
||||
REST URL, GraphQL document/hash, protobuf bytes와 generated message serialization은
|
||||
query key가 아니다. transport 교체가 use-case identity를 바꾸지 않으면 같은
|
||||
application query family를 유지할 수 있지만, old/new representation을 한 cache
|
||||
entry에 shadow write하지 않는다.
|
||||
|
||||
network retry는 transport adapter가 소유하고 Query retry는 기본 `false`다.
|
||||
refresh failure에서 유효한 previous data는 stale-degraded로 유지한다. schema,
|
||||
mapper, scope, authorization와 contract mismatch는 stale data를 계속 노출해도
|
||||
되는지 query profile이 명시해야 하며 기본은 security-sensitive scope에서
|
||||
즉시 숨김/clear다.
|
||||
|
||||
VD-13이 scope/persistence profile, normative key layout, account/session
|
||||
generation과 late-result fence를 소유한다. 이 문서는 그 profile을 exact join하고
|
||||
operation/cache policy, pagination, revalidation과 mutation coherence를 소유한다.
|
||||
|
||||
## 14. Pagination과 conditional revalidation
|
||||
|
||||
cursor pagination은 다음 binding을 갖는다.
|
||||
|
||||
```text
|
||||
PaginationBinding
|
||||
query family fingerprint
|
||||
canonical filters/sort
|
||||
scope fingerprint
|
||||
server snapshot/revision policy
|
||||
page size ceiling
|
||||
opaque next cursor
|
||||
```
|
||||
|
||||
- cursor를 decode하거나 URL로 취급하지 않는다.
|
||||
- `hasMore === (nextCursor !== null)`을 codec에서 강제한다.
|
||||
- single-page cache는 runtime-scoped cursor fingerprint를 semantic key에 포함하고,
|
||||
infinite query만 cursor를 root key에서 제외해 bounded `pageParam`으로 둔다.
|
||||
- max pages/items/estimated bytes를 넘으면 더 불러오지 않는다.
|
||||
- 동일 cursor 반복, loop와 non-progress page를 contract failure로 닫는다.
|
||||
- offset pagination의 insert/delete drift를 자동 deduplicate로 숨기지 않는다.
|
||||
- page merge는 mapper가 보장한 stable identity가 있을 때만 deterministic하다.
|
||||
- previous filters의 page를 새 filter key에 재사용하지 않는다.
|
||||
|
||||
REST `304`는 cached data가 있다는 뜻이 아니라 representation이 바뀌지 않았다는
|
||||
transport 결과다. exact query fingerprint, scope/generation과 cached mapped
|
||||
value에 binding된 validator record가 모두 있을 때만 freshness를 갱신한다.
|
||||
cached value가 없거나 binding이 다르면 unconditional request를 한 번 수행하거나
|
||||
closed failure로 끝낸다.
|
||||
|
||||
GraphQL persisted operation과 gRPC-Web unary는 기본적으로 application-level
|
||||
validator가 없다. backend가 revision을 제공하면 response schema/mapper가 opaque
|
||||
revision을 application revalidation policy로 투영해야 하며 HTTP/gRPC metadata를
|
||||
임의로 ETag처럼 해석하지 않는다.
|
||||
|
||||
## 15. Mutation coherence
|
||||
|
||||
mutation과 query cache는 server commit authority가 아니다.
|
||||
|
||||
```text
|
||||
validate command
|
||||
-> derive logical key + exact command equality/opaque identity token
|
||||
-> runtime/scope coordinator applies concurrency + duplicate admission
|
||||
-> separately acquire invalidation-topic hint-coalescing lease
|
||||
-> cancel exact affected query reads
|
||||
-> capture bounded base cache revision and inverse patch
|
||||
-> install own ordered optimistic layer with revision CAS
|
||||
-> transport derives backend idempotency binding from operation policy
|
||||
-> execute command once with transport-owned retry policy
|
||||
-> success: registered exact seed/compare-and-apply + list/aggregate invalidation
|
||||
-> rejection: remove or invert only own layer with revision CAS
|
||||
-> uncertainty/CAS miss: preserve other commits + invalidate/authoritative refetch
|
||||
-> release invalidation lease + concurrency admission
|
||||
```
|
||||
|
||||
- duplicate submit 정책은 `JOIN_IDENTICAL`, `REJECT_DUPLICATE`,
|
||||
`ALLOW_INDEPENDENT` 중 operation별로 고정하고, 동일성은 전체 validated semantic
|
||||
input의 runtime-private exact equality guard와 opaque identity token으로
|
||||
판정한다.
|
||||
- 같은 hook instance의 Promise dedupe를 server idempotency로 간주하지 않는다.
|
||||
- logical key/identity admission은 local ordering이고 invalidation-topic lease는
|
||||
remote hint coalescing일 뿐이다. 둘 다 backend idempotency authority가 아니다.
|
||||
- optimistic patch는 raw DTO나 generated message를 만들지 않는다.
|
||||
- snapshot item/byte ceiling을 넘으면 optimistic update를 하지 않고 pending UX만
|
||||
제공한다.
|
||||
- 409/GraphQL conflict code/gRPC `ABORTED`는 동일한 safe conflict vocabulary로
|
||||
mapping하되 server revision과 merge policy는 feature use case가 소유한다.
|
||||
- server가 commit한 뒤 local invalidation 실패를 command 실패로 되돌리지 않는다.
|
||||
cache health를 degraded로 기록하고 bounded refetch/recovery를 예약한다.
|
||||
- command의 자동 protocol failover는 중복 side effect 위험 때문에 금지한다.
|
||||
|
||||
## 16. Server streaming과 cache
|
||||
|
||||
gRPC-Web server stream, GraphQL subscription과 incremental delivery는 ordinary
|
||||
queryFn과 다르다.
|
||||
|
||||
현재 installed API operation registry는 terminal REST operation만 소유한다.
|
||||
GraphQL HTTP와 gRPC-Web unary/server-stream은 각 reference adapter가 구현될 때
|
||||
protocol discriminant와 전용 binding으로 확장한다. SSE, WebSocket과 Web Push는
|
||||
VD-28 realtime registry가 소유하며 bounded polling은 registered terminal REST
|
||||
`QUERY`의 scheduling policy이지 새 protocol이나 automatic failover가 아니다.
|
||||
GraphQL `@defer`/`@stream`은 선택될 경우에도 한 HTTP operation의 finite
|
||||
incremental response이며 subscription과 같은 장기 realtime stream이 아니다.
|
||||
|
||||
```text
|
||||
ServerStreamPort
|
||||
open(frozen request, signal)
|
||||
-> AsyncIterable<Result<MappedEvent>>
|
||||
-> close()
|
||||
```
|
||||
|
||||
- frame/event마다 schema, mapper, scope와 generation을 재검증한다.
|
||||
- sequence, duplicate, gap과 resume token은 backend application protocol이다.
|
||||
- bounded queue, high-water mark, overflow, idle/total deadline을 선언한다.
|
||||
- stream event는 registered reducer로 immutable snapshot을 만들거나 query
|
||||
invalidation hint만 발행한다.
|
||||
- partial event를 ordinary query success로 cache하지 않는다.
|
||||
- stream 종료/재connect를 TanStack Query retry로 처리하지 않는다.
|
||||
|
||||
GraphQL subscription은 현재 `NOT_SELECTED`이고, gRPC-Web server-stream은
|
||||
`DESIGNED_NOT_IMPLEMENTED`다.
|
||||
|
||||
gRPC-Web `ServerStreamPort`는 operation-bound outbound stream result일 수 있다.
|
||||
API adapter가 protobuf frame/message decode, semantic schema와 boundary mapper를
|
||||
끝낸 뒤 제품이 runtime-wide notification projection을 명시적으로 선택한
|
||||
branch에서만 mapped event를 VD-28 common coordinator/`FeatureEventInput`에
|
||||
전달한다. protobuf를 `REALTIME_EVENT_V1` JSON으로 감싸지 않고, 첫 event 전
|
||||
opening replay와 이후 resume 규칙은 VD-27이 계속 소유한다.
|
||||
|
||||
## 17. Backend/provider 계약
|
||||
|
||||
구현 owner, 권장 topology, 현재 reference endpoint/envelope, idempotency store,
|
||||
Cursor/ETag/revision과 protocol별 handoff checklist는
|
||||
[Backend API와 Server State contract](./backend-api-and-server-state-contract.md)가
|
||||
소유한다. 아래 표는 frontend 설계가 요구하는 경계 요약이다.
|
||||
|
||||
| 경계 | backend/provider가 제공할 계약 |
|
||||
| --- | --- |
|
||||
| 공통 | authorization, contract version/artifact compatibility, encoded transfer와 decoded payload의 bounded owner, stable error code, correlation/trace projection, idempotency와 rate-limit semantics |
|
||||
| REST | exact method/path/media/status/envelope, CSRF strategy, idempotency retention, cursor binding, ETag/If-None-Match 또는 revision, retry-safe status와 CORS/cache policy |
|
||||
| GraphQL | schema registry, persisted-operation manifest, allowlist/cost/depth enforcement, safe error extension vocabulary, operation retirement과 N/N-1 router rollout |
|
||||
| Connect | proto/descriptor/codegen source, exact Connect-Web runtime/encoding/method, EndStream/status/error, timeout/cancel/compression/CORS와 browser/server conformance |
|
||||
| gRPC-Web | proto/descriptor source, Buf/protoc compatibility policy, gRPC-Web proxy, exact service/method, message/frame ceiling, status/trailer/CORS exposure와 stream resume protocol |
|
||||
| Protobuf REST Gateway | selected gateway kind/version, HttpRule/ProtoJSON/OpenAPI artifact, path/query/body/status/error/header mapping, edge→upstream cancellation과 N/N-1 conformance |
|
||||
| Schema | authenticated source artifact, additive/breaking classification, deprecation window, fixtures and source digest |
|
||||
| Mapper | domain meaning, temporal/numeric/null/enum semantics와 stable identity |
|
||||
| Cache | revision/conflict/idempotency/invalidation semantics; frontend TTL은 authorization 대체가 아님 |
|
||||
|
||||
frontend가 제공하는 Zod schema, generated type과 cache invalidation은 backend
|
||||
authorization, validation, idempotency와 conflict resolution을 대체하지 않는다.
|
||||
|
||||
## 18. Security와 privacy
|
||||
|
||||
- API base/GraphQL/Connect/gRPC-Web endpoint는 HTTPS registry ID로 고정한다.
|
||||
- caller가 URL, header, GraphQL document, service/method와 metadata를 제출하지
|
||||
못한다.
|
||||
- auth owner가 credential을 붙이고 application/query/cache에는 token을 노출하지
|
||||
않는다.
|
||||
- credential attachment 뒤 URL/method/origin/body digest와 registry-owned header
|
||||
binding을 재검증한다. auth integration unavailable 상태에서 request를 보내지
|
||||
않는다.
|
||||
- cookie session의 unsafe request는 CSRF token/header 또는 same-site BFF 정책을
|
||||
operation profile과 provider conformance로 증명한다.
|
||||
- GET/GraphQL variables에 sensitive filter를 넣는 operation은 별도 review 없이
|
||||
만들지 않는다.
|
||||
- response byte/depth/node/string/collection/frame cap으로 resource exhaustion을
|
||||
막는다.
|
||||
- GraphQL cost/depth와 gRPC message cap은 server/proxy에서도 강제한다.
|
||||
- mapper와 cache는 prototype/accessor/class/native object를 받아들이지 않는다.
|
||||
- PII/business ID를 query key, diagnostics label, persisted cache physical key에
|
||||
직접 넣지 않는다. 필요한 identity는 opaque partition/token policy를 쓴다.
|
||||
- command identity token/logical mutation key는 runtime-local control data이며
|
||||
diagnostics, cross-context wire와 persistence에 넣지 않는다.
|
||||
- raw request/response, GraphQL variables/errors, protobuf bytes/metadata, ETag,
|
||||
cursor, idempotency key와 validation value를 관측 데이터에 넣지 않는다.
|
||||
|
||||
## 19. Observability
|
||||
|
||||
허용된 bounded aggregate:
|
||||
|
||||
- operation registry ID, protocol, semantics
|
||||
- outcome/error kind, HTTP status group 또는 gRPC status code allowlist
|
||||
- GraphQL full/partial/rejected outcome
|
||||
- attempt/deadline/duration/encoded-byte/result-item/frame bucket
|
||||
- schema/mapper profile ID와 compatibility outcome
|
||||
- query hit/miss/stale/refetch/eviction bucket
|
||||
- mutation optimistic/rollback/conflict/invalidation outcome
|
||||
- provider/browser/runtime version의 low-cardinality bucket
|
||||
|
||||
금지:
|
||||
|
||||
- URL/path parameter/search/body/header
|
||||
- GraphQL document, variables, response path와 raw error message
|
||||
- protobuf message/metadata/trailer raw value
|
||||
- resource/account/tenant ID, cursor, validator, digest와 cache value
|
||||
|
||||
하나의 logical operation은 terminal observation 하나를 만든다. attempt span은
|
||||
sampling된 내부 detail로만 남기며 terminal success/failure count를 중복시키지
|
||||
않는다.
|
||||
|
||||
## 20. Failure와 fallback
|
||||
|
||||
| 실패 | 기본 결과 |
|
||||
| --- | --- |
|
||||
| registry/schema/mapper 누락 | network 전 fail-closed, operation traffic disable |
|
||||
| incompatible contract artifact | product mount 또는 해당 capability admission 차단 |
|
||||
| response cap/decode/schema mismatch | body/reader cancel, cache write 금지, provider incompatibility |
|
||||
| mapper violation | cache write 금지, safe contract failure |
|
||||
| REST retry exhaustion | stale 허용 profile만 previous data 유지 |
|
||||
| GraphQL persisted operation missing | full document fallback 금지, coherent artifact rollback |
|
||||
| GraphQL partial data | default reject; explicit profile만 completeness와 함께 사용 |
|
||||
| Connect missing/duplicate EndStream 또는 oversize whole body | call cancel, cache write 금지, provider/runtime incompatible |
|
||||
| gRPC-Web proxy/trailer mismatch | reader cancel, provider unavailable/incompatible |
|
||||
| REST Gateway HttpRule/OpenAPI/runtime drift | affected route admission 차단, coherent gateway artifact rollback |
|
||||
| server stream gap/overflow | snapshot 폐기 또는 authoritative refetch |
|
||||
| account/generation mismatch | late result 폐기, old-scope cache write 금지 |
|
||||
| invalidation failure after commit | command 성공 유지, cache degraded + recovery refetch |
|
||||
|
||||
GraphQL, Connect 또는 gRPC-Web failure를 REST로 자동 전환하지 않는다. 사전에 등록된
|
||||
read-only shadow/fallback operation이 있고 동일 authorization/mapper/result
|
||||
contract를 conformance suite로 증명한 경우만 selector가 새 logical query를
|
||||
시작할 수 있다.
|
||||
|
||||
## 21. Rollout과 removal
|
||||
|
||||
```text
|
||||
ADR + registry schema accepted
|
||||
-> deterministic codec/schema/mapper fixture
|
||||
-> provider-neutral adapter and fake
|
||||
-> negative boundary/removal gate
|
||||
-> AVAILABLE_NOT_COMPOSED
|
||||
-> product/provider/operation selection
|
||||
-> bootstrap composition behind TrafficAdmission=DISABLED
|
||||
-> COMPOSED
|
||||
-> shadow/read-only conformance
|
||||
-> browser/provider/operations evidence
|
||||
-> PromotionEvidence=COMPLETE
|
||||
-> CANARY
|
||||
-> ENABLED
|
||||
```
|
||||
|
||||
REST v2 local baseline은 installed reference operation에 연결됐다. 실제 provider
|
||||
traffic은 operation/profile 단위의 conformance와 canary를 거쳐야 하며,
|
||||
conditional/pagination은 backend 계약 없이 enabled하지 않는다.
|
||||
GraphQL/Connect/gRPC-Web/codegen/gateway dependency는 실제 selected operation이
|
||||
없으면 production inventory에 없어야 한다.
|
||||
|
||||
removal:
|
||||
|
||||
1. 신규 operation admission을 닫는다.
|
||||
2. query는 cancel하고 command/stream은 bounded drain 또는 explicit abort한다.
|
||||
3. 해당 invalidation listener, auth attachment와 provider를 close한다.
|
||||
4. current scope의 mapped memory cache를 clear한다.
|
||||
5. operation/schema/mapper/query profile과 generated artifact를 제거한다.
|
||||
6. dependency, config, proxy route, test fixture와 production module inventory가
|
||||
함께 제거됐음을 증명한다.
|
||||
7. backend persisted-operation/method retirement은 N/N-1 client window 뒤에 한다.
|
||||
|
||||
## 22. Test와 promotion evidence
|
||||
|
||||
### Deterministic
|
||||
|
||||
- operation registry closed union/reference/orphan/duplicate
|
||||
- request canonicalization과 query-key identity
|
||||
- timeout/total deadline/retry/idempotency/auth recovery
|
||||
- response byte/depth/node/collection cap
|
||||
- schema unknown-field, scalar, null/enum/numeric/date matrix
|
||||
- mapper success/failure/no raw value leakage
|
||||
- query stale/gc/refetch/pagination/mutation/rollback/generation fence
|
||||
|
||||
### Contract
|
||||
|
||||
- REST OpenAPI/envelope/status/media/cursor/conditional/idempotency
|
||||
- GraphQL schema + persisted manifest + variables/result/error/partial policy
|
||||
- proto descriptor + breaking check + gRPC status/trailer/frame fixture
|
||||
- Connect unary/stream JSON/binary/GET/EndStream/CORS/deadline fixture
|
||||
- HttpRule route manifest + ProtoJSON/OpenAPI/status/error/header mapping fixture
|
||||
- 같은 fixture를 fake, emulator/staging과 actual provider에 실행
|
||||
|
||||
### Browser/integration
|
||||
|
||||
- bootstrap → feature → transport → schema → mapper → Query → UI
|
||||
- AbortSignal/navigation/logout/account switch
|
||||
- CORS/cookie/CSRF/redirect/content-encoding
|
||||
- HTTP/2/proxy/CDN/Connect EndStream/gRPC-Web trailer behavior
|
||||
- offline/reconnect/focus와 stale-degraded UI
|
||||
|
||||
### Fault
|
||||
|
||||
- truncated/oversize/malformed response
|
||||
- slow credential/network/body/schema/mapper phase
|
||||
- 401 recovery, 429, retry exhaustion과 total deadline
|
||||
- GraphQL partial/error/persisted-operation drift
|
||||
- Connect missing/early/duplicate EndStream, whole-body overflow와 compression mismatch
|
||||
- gRPC missing/conflicting terminal status source, corrupt/compressed/oversize
|
||||
frame와 stream gap
|
||||
- REST Gateway route/OpenAPI/runtime rewrite drift와 abort propagation loss
|
||||
- late response, duplicate mutation, optimistic rollback과 invalidation failure
|
||||
|
||||
### Operations
|
||||
|
||||
- operation kill switch
|
||||
- contract artifact N/N-1 rollout과 rollback
|
||||
- provider incompatibility containment
|
||||
- cache scope reset와 stale-data decision
|
||||
- generated client/GraphQL/Connect/gRPC-Web/REST Gateway removal drill
|
||||
|
||||
fake와 generated compile success만으로 actual provider, browser나 operations
|
||||
evidence를 `COMPLETE`로 표시하지 않는다.
|
||||
|
||||
## 23. 설계 우선 work package
|
||||
|
||||
| package | 목표 |
|
||||
| --- | --- |
|
||||
| API-01 | REST v2 operation registry, total deadline, bounded decoder와 conditional/pagination contract |
|
||||
| API-02 | multi-protocol schema artifact/digest governance와 typed Mapper result |
|
||||
| API-03 | strict ServerStateProfile, query-key codec, pagination/revalidation와 mutation policy |
|
||||
| API-04 | persisted-operation-only GraphQL reference adapter와 conformance harness |
|
||||
| API-05 | gRPC-Web unary/server-stream reference adapter와 proxy harness |
|
||||
| API-06 | Connect-Web unary/server-stream reference adapter와 provider harness |
|
||||
| API-07 | Protobuf governance와 selected REST Gateway conformance |
|
||||
| API-08 | atomic composition, readiness, kill switch, runbook와 provider/browser evidence |
|
||||
|
||||
권장 순서는 API-01 → API-02 → API-03이다. API-04~07은 제품 선택과
|
||||
backend/provider 계약이 생긴 branch만 독립적으로 시작한다. GraphQL, Connect,
|
||||
gRPC-Web과 REST Gateway를 “미래 대비” 목적으로 모두 기본 bundle에 설치하지
|
||||
않는다.
|
||||
|
||||
## 24. 완료 기준
|
||||
|
||||
- [ ] 모든 installed operation은 protocol/schema/mapper/cache/error/deadline owner가 있다.
|
||||
- [ ] application/presentation public type에 DTO, GraphQL SDK와 generated protobuf가 없다.
|
||||
- [ ] untrusted byte부터 mapped projection까지 모든 ceiling과 trust transition이 닫혀 있다.
|
||||
- [ ] query key와 실제 request input이 동일 canonical source에서 파생된다.
|
||||
- [ ] transport retry와 Query retry가 중복되지 않는다.
|
||||
- [ ] command idempotency, optimistic patch와 conflict/invalidation owner가 명시돼 있다.
|
||||
- [ ] account/logout/release generation 뒤 late result가 cache에 들어가지 않는다.
|
||||
- [ ] actual REST/GraphQL/Connect/gRPC-Web/Gateway provider에 같은 semantic
|
||||
conformance fixture를 실행한다.
|
||||
- [ ] contract drift, kill switch, rollback과 optional dependency removal drill이 통과한다.
|
||||
- [ ] raw payload/URL/document/message/metadata/validator가 log와 cache에 없다.
|
||||
- [ ] `COMPOSED`와 production-ready/provider-conformant를 같은 의미로 쓰지 않는다.
|
||||
|
||||
## 25. 관련 문서
|
||||
|
||||
- [Frontend ports, adapters, and boundaries](./frontend-ports-adapters-and-boundaries.md)
|
||||
- [Client cache and browser storage](./client-cache-and-storage.md)
|
||||
- [VD-13 Client cache scope와 persistence](./decisions/VD-13-client-cache-scope-and-persistence.md)
|
||||
- [Protobuf browser transport와 REST Gateway](./protobuf-browser-transport-and-rest-gateway.md)
|
||||
- [VD-29 Connect-Web와 browser Protobuf runtime](./decisions/VD-29-connect-web-and-browser-protobuf-runtime.md)
|
||||
- [VD-30 Protobuf contract와 REST Gateway](./decisions/VD-30-protobuf-contract-and-rest-gateway.md)
|
||||
- [Contract compatibility](../contracts/compatibility.md)
|
||||
- [Frontend platform testing strategy](../testing/frontend-platform-testing-strategy.md)
|
||||
@@ -0,0 +1,710 @@
|
||||
# Backend API와 Server State handoff contract
|
||||
|
||||
- 상태: frontend handoff design accepted, backend implementation/evidence pending
|
||||
- 기준일: 2026-07-28
|
||||
- 대상: Web API/BFF, application service, persistence, identity, GraphQL router,
|
||||
Connect/gRPC-Web gateway, Protobuf REST Gateway와 운영 owner
|
||||
- frontend 기준:
|
||||
[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)
|
||||
- 파일 전송 backend 기준:
|
||||
[Server file capability infrastructure](./server-file-capability-infrastructure.md)
|
||||
- 복구 절차:
|
||||
[API contract와 server-state recovery](../operations/api-contract-and-server-state-recovery.md)
|
||||
|
||||
## 1. 문서의 경계
|
||||
|
||||
이 문서는 이 frontend template이 실제 제품 backend와 연결될 때 backend가
|
||||
제공해야 하는 구조, wire contract, 상태 의미와 운영 증거를 정의한다. 특정
|
||||
언어·framework·cloud 제품을 강제하지 않는다. Spring, Nest/Fastify, Go,
|
||||
.NET 또는 다른 stack을 사용해도 아래 불변조건은 동일하다.
|
||||
|
||||
이 repository에는 backend source, database migration, identity provider,
|
||||
GraphQL schema/router, protobuf descriptor, Connect/gRPC-Web runtime/proxy,
|
||||
REST transcoder와 실제 provider evidence가 없다. 따라서 이 문서는 backend 구현
|
||||
완료 증거가 아니다.
|
||||
|
||||
파일 업로드·다운로드, object storage, presigned URL, multipart와 Image CDN은
|
||||
별도 server file 문서가 소유한다. 이 문서는 ordinary REST/GraphQL/Connect/
|
||||
gRPC-Web application API, Protobuf REST Gateway와 frontend Server State 계약만
|
||||
소유한다.
|
||||
|
||||
## 2. 권장 논리 구조
|
||||
|
||||
```text
|
||||
Browser
|
||||
-> CDN / reverse proxy / WAF
|
||||
-> Browser-facing Web API or BFF
|
||||
-> authentication + authorization
|
||||
-> exact operation registry
|
||||
-> request schema / byte / rate limit
|
||||
-> REST controller
|
||||
-> optional persisted GraphQL router
|
||||
-> optional Connect browser RPC gateway
|
||||
-> optional gRPC-Web gateway
|
||||
-> optional Protobuf REST transcoder
|
||||
-> application service
|
||||
-> command transaction
|
||||
-> query/read-model service
|
||||
-> idempotency coordinator
|
||||
-> revision/validator owner
|
||||
-> outbox/event owner
|
||||
-> primary database
|
||||
-> idempotency store
|
||||
-> read replica/read model
|
||||
-> event broker when selected
|
||||
```
|
||||
|
||||
Browser-facing contract owner와 내부 service contract owner를 분리한다.
|
||||
|
||||
- Browser API/BFF는 CORS, cookie/CSRF 또는 bearer, public DTO, envelope,
|
||||
body ceiling, status/media와 redaction을 소유한다.
|
||||
- Application service는 authorization 재검사, transaction, idempotency,
|
||||
conflict, revision과 domain invariant를 소유한다.
|
||||
- Persistence adapter는 SQL/NoSQL/Redis vendor type, row version과 cursor
|
||||
implementation을 외부 DTO에 노출하지 않는다.
|
||||
- GraphQL router, Connect/gRPC-Web gateway와 REST transcoder는 선택 adapter다.
|
||||
다른 protocol로 임의 fallback하거나 frontend에 내부 service address를
|
||||
노출하지 않는다.
|
||||
|
||||
작은 제품은 이 논리 모듈을 하나의 deployable로 구현할 수 있다. deployable을
|
||||
나누는 것보다 transaction/idempotency/authorization owner가 하나로 명확한지가
|
||||
우선이다.
|
||||
|
||||
## 3. 공통 contract artifact
|
||||
|
||||
Backend와 frontend release는 다음 bounded contract set을 공유한다.
|
||||
|
||||
```text
|
||||
ApiContractSetV1
|
||||
globalApiContractVersion
|
||||
restArtifactId + digest
|
||||
runtimeSchemaManifestId + digest
|
||||
mapperSemanticManifestId + digest
|
||||
errorVocabularyVersion
|
||||
minimumFrontendVersion
|
||||
minimumBackendVersion
|
||||
effectiveAt
|
||||
retirementEpoch | null
|
||||
|
||||
optional:
|
||||
graphqlSchemaId + digest
|
||||
persistedGraphqlManifestId + digest
|
||||
protobufDescriptorId + digest
|
||||
protobufSourceOrModuleId + digest
|
||||
protobufCodegenProfileId
|
||||
connectProviderProfileId
|
||||
grpcWebProviderProfileId
|
||||
protobufRestGatewayProfileId
|
||||
httpRuleArtifactId + digest
|
||||
protoJsonProfileId
|
||||
gatewayOpenApiArtifactId + digest
|
||||
```
|
||||
|
||||
최소 산출물:
|
||||
|
||||
- authenticated OpenAPI 또는 동등한 REST schema source
|
||||
- exact status/media/envelope fixture
|
||||
- stable error code vocabulary
|
||||
- request/response byte와 collection ceiling
|
||||
- scalar/date/null/enum 의미
|
||||
- N/N-1 compatibility 결과
|
||||
- backend build와 frontend release가 참조하는 immutable digest
|
||||
|
||||
runtime config의 version 문자열 일치만 compatibility 증거로 사용하지 않는다.
|
||||
artifact digest가 없는 동안에는 실제 staging conformance fixture와 수동 승인
|
||||
evidence가 필요하다.
|
||||
|
||||
## 4. 현재 reference REST 계약
|
||||
|
||||
현재 frontend에 실제 조립된 operation은 다음 세 개다.
|
||||
|
||||
| operation | request | success |
|
||||
| --- | --- | --- |
|
||||
| `LIST_REFERENCE_RESOURCES` | `GET /api/reference-resources?cursor&limit&tags` | `200 application/json` |
|
||||
| `GET_REFERENCE_RESOURCE` | `GET /api/reference-resources/{resourceId}` | `200 application/json` |
|
||||
| `CREATE_REFERENCE_RESOURCE` | `POST /api/reference-resources` | `200` 또는 `201 application/json` |
|
||||
|
||||
현재 list payload는 `ReferenceResource[]`다. `cursor` 입력이 존재하더라도
|
||||
`CursorPage` 출력 계약은 아직 아니다. backend가 같은 operation에서 배열을
|
||||
page object로 조용히 바꾸면 schema mismatch로 실패한다.
|
||||
|
||||
resource DTO:
|
||||
|
||||
```text
|
||||
ReferenceResourceDtoV1
|
||||
id: non-empty string, maximum 120 characters
|
||||
name: non-empty string, maximum 240 characters
|
||||
createdAt?: RFC 3339 date-time
|
||||
```
|
||||
|
||||
create command:
|
||||
|
||||
```text
|
||||
CreateReferenceResourceCommandV1
|
||||
name: trimmed string, 1..120
|
||||
note?: trimmed string, 0..500
|
||||
```
|
||||
|
||||
Backend는 frontend validation을 신뢰하지 않고 동일하거나 더 좁은 validation과
|
||||
authorization을 다시 수행한다.
|
||||
|
||||
### 4.1 JSON envelope
|
||||
|
||||
모든 현재 JSON success/failure는 다음 envelope를 사용한다.
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {},
|
||||
"meta": {
|
||||
"requestId": "server-request-id",
|
||||
"traceId": "server-trace-id",
|
||||
"correlationId": "client-correlation-id"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "STABLE_MACHINE_CODE",
|
||||
"category": "optional-safe-category",
|
||||
"message": "optional non-sensitive copy",
|
||||
"retryable": false,
|
||||
"details": {}
|
||||
},
|
||||
"meta": {
|
||||
"requestId": "server-request-id",
|
||||
"traceId": "server-trace-id",
|
||||
"correlationId": "client-correlation-id"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Envelope 최상위 unknown field는 현재 거절된다. ordinary resource DTO의 unknown
|
||||
field는 frontend schema에서 strip되지만 additive compatibility는 contract
|
||||
review와 fixture를 먼저 통과해야 한다.
|
||||
|
||||
`requestId`, `traceId`, `correlationId`는 각각 1..128 범위의 안전한 opaque
|
||||
identifier다. credential, user data, cursor, validator와 database key를
|
||||
identifier에 encode하지 않는다.
|
||||
|
||||
### 4.2 Status와 error
|
||||
|
||||
| HTTP status | 의미 |
|
||||
| --- | --- |
|
||||
| `400` | malformed request 또는 closed request contract 위반 |
|
||||
| `401` | 인증 없음/만료. 이미 적용된 command를 401로 반환하지 않음 |
|
||||
| `403` | authenticated principal에게 권한 없음 |
|
||||
| `404` | authorization 정책상 공개 가능한 not-found |
|
||||
| `409` | idempotency fingerprint, domain revision 또는 semantic conflict |
|
||||
| `412` | selected conditional write의 `If-Match` precondition 실패 |
|
||||
| `422` | field validation. bounded `details.issues[]`만 허용 |
|
||||
| `429` | rate limit. 유효한 `Retry-After`와 operation 정책 제공 |
|
||||
| `500/502/503/504` | server/provider failure. command effect certainty 별도 |
|
||||
|
||||
현재 frontend의 ordinary status mapper는 `412` 전용 처리를 아직 연결하지
|
||||
않았다. conditional mutation을 선택할 때 frontend failure vocabulary와
|
||||
transaction을 함께 승격해야 한다.
|
||||
|
||||
Backend `error.code`는 machine-readable stable code다. stack, SQL/vendor error,
|
||||
raw validation value, authorization reason과 내부 service address를 반환하지
|
||||
않는다.
|
||||
|
||||
## 5. 인증, CSRF와 CORS
|
||||
|
||||
현재 reference operation은 다음 profile로 조립돼 있다.
|
||||
|
||||
```text
|
||||
auth = external bearer
|
||||
Authorization: Bearer <credential>
|
||||
fetch credentials = omit
|
||||
CSRF profile = none
|
||||
redirect = error
|
||||
referrer policy = no-referrer
|
||||
```
|
||||
|
||||
Backend/BFF는 bearer의 issuer, audience, signature algorithm, time claims와
|
||||
revocation/session policy를 검증하고 operation별 authorization을 적용한다.
|
||||
401과 403을 구분하며 frontend cache를 authorization authority로 사용하지 않는다.
|
||||
|
||||
쿠키 session으로 전환할 경우 같은 profile로 간주하지 않는다. 별도
|
||||
`SAME_ORIGIN_COOKIE` profile에 다음을 함께 승인한다.
|
||||
|
||||
- `Secure`, `HttpOnly`, 명시적 `SameSite`와 host/path scope
|
||||
- unsafe method의 CSRF token/header와 Origin/Sec-Fetch-Site 검증
|
||||
- credentialed CORS에서 wildcard origin 금지
|
||||
- login/logout/session rotation과 cache generation 전환
|
||||
- session fixation, token rotation과 concurrent tab 동작
|
||||
|
||||
Cross-origin bearer provider 최소 CORS:
|
||||
|
||||
- exact allow-origin 목록과 bounded preflight cache
|
||||
- `Authorization`, `Content-Type`, `Idempotency-Key`,
|
||||
`X-Correlation-ID`, 향후 `If-None-Match`, `If-Match` 허용
|
||||
- 필요한 경우 `ETag`, `Retry-After`, request/trace header만 expose
|
||||
- redirect login page, HTML error body와 wildcard credential 금지
|
||||
|
||||
## 6. Command와 idempotency
|
||||
|
||||
`CREATE_REFERENCE_RESOURCE`는 keyed command다. frontend memory single-flight는
|
||||
backend idempotency를 대체하지 않는다.
|
||||
|
||||
idempotency identity:
|
||||
|
||||
```text
|
||||
principal/tenant
|
||||
+ semantic operation ID and contract version
|
||||
+ Idempotency-Key
|
||||
+ canonical request fingerprint
|
||||
```
|
||||
|
||||
권장 record:
|
||||
|
||||
```text
|
||||
IdempotencyRecord
|
||||
principalFingerprint
|
||||
operationId
|
||||
contractVersion
|
||||
idempotencyKeyHash
|
||||
requestFingerprint
|
||||
state = IN_PROGRESS | COMMITTED | FAILED_SAFE | EFFECT_UNKNOWN
|
||||
responseStatus
|
||||
responseEnvelopeReference
|
||||
resourceRevision | null
|
||||
leaseOwner + leaseExpiry
|
||||
retentionExpiry
|
||||
createdAt + completedAt
|
||||
```
|
||||
|
||||
불변조건:
|
||||
|
||||
- claim과 command transaction의 관계가 원자적이거나 crash reconciliation
|
||||
가능해야 한다.
|
||||
- 같은 key와 같은 fingerprint replay는 같은 authoritative receipt를 반환한다.
|
||||
- 같은 key와 다른 fingerprint는 `409 IDEMPOTENCY_KEY_REUSED`다.
|
||||
- concurrent replay는 하나만 실행하고 나머지는 같은 result를 기다리거나
|
||||
bounded `IN_PROGRESS` 결과를 받는다.
|
||||
- commit 뒤 response 유실은 새 resource를 만들지 않는다.
|
||||
- `EFFECT_UNKNOWN`은 새 key로 자동 재시도하지 않고 status/reconcile endpoint로
|
||||
확인한다.
|
||||
- retention은 frontend retry/recovery 최대 window보다 길고 quota/abuse limit이
|
||||
있다.
|
||||
- key 원문과 request body를 log/metric label에 넣지 않는다.
|
||||
|
||||
Database unique constraint 또는 durable compare-and-set이 최종 중복 방지
|
||||
authority여야 한다. process-local map/lock만 사용하지 않는다.
|
||||
|
||||
## 7. Cursor pagination과 snapshot
|
||||
|
||||
Backend가 pagination을 선택할 때 새 response schema/operation version으로 다음
|
||||
contract를 제공한다.
|
||||
|
||||
```text
|
||||
CursorPage<T>
|
||||
items: T[]
|
||||
nextCursor: opaque string | null
|
||||
hasMore: boolean
|
||||
snapshotToken: opaque string | null
|
||||
```
|
||||
|
||||
필수 불변조건:
|
||||
|
||||
- `hasMore === (nextCursor !== null)`
|
||||
- 동일 chain의 `snapshotToken`은 모든 page에서 동일
|
||||
- cursor는 principal/tenant, filter, sort, contract version과 snapshot에 binding
|
||||
- cursor는 opaque, 무결성 보호, 만료와 key rotation 정책 보유
|
||||
- offset이 아니라 stable keyset ordering 사용
|
||||
- total order의 마지막 tie-breaker는 immutable unique ID
|
||||
- deleted/inserted row가 duplicate/gap을 만드는 의미를 snapshot 정책으로 결정
|
||||
- empty page인데 `hasMore=true`인 sparse page 허용 여부를 operation profile에 고정
|
||||
- cursor 최대 encoded byte, page size와 total scan/cost ceiling을 server도 강제
|
||||
- invalid, expired, wrong-principal, wrong-filter cursor의 safe error code를 고정
|
||||
|
||||
권장 query ordering 예:
|
||||
|
||||
```text
|
||||
ORDER BY created_at DESC, resource_id DESC
|
||||
cursor payload = version + snapshot watermark + last(created_at, resource_id)
|
||||
+ filter digest + principal/tenant binding + expiry
|
||||
```
|
||||
|
||||
Cursor 원문은 log, trace, analytics와 frontend persistent storage에 넣지 않는다.
|
||||
|
||||
### 7.1 배열에서 page로의 migration
|
||||
|
||||
1. `ReferenceResourceListPagePayloadV2` schema와 새 operation/version을 추가한다.
|
||||
2. backend가 N/N-1 동안 기존 배열과 page contract를 동시에 제공한다.
|
||||
3. frontend가 cursor runtime을 새 bound query/infinite query에 연결한다.
|
||||
4. loop/snapshot/ceiling/abort conformance를 staging에서 검증한다.
|
||||
5. 새 operation을 canary한 뒤 기존 배열 operation을 retirement한다.
|
||||
|
||||
동일 media/status에서 payload shape만 바꾸는 in-place migration은 금지한다.
|
||||
|
||||
## 8. Conditional read와 revision/CAS
|
||||
|
||||
### 8.1 Read validator
|
||||
|
||||
Backend가 application-managed revalidation을 선택하면 exact mapped
|
||||
representation마다 ETag를 제공한다.
|
||||
|
||||
```text
|
||||
GET without validator
|
||||
-> 200 + JSON envelope + ETag
|
||||
|
||||
GET with If-None-Match
|
||||
-> representation unchanged: 304 + empty body
|
||||
-> changed: 200 + JSON envelope + new ETag
|
||||
```
|
||||
|
||||
불변조건:
|
||||
|
||||
- validator는 principal/tenant, authorization-visible representation,
|
||||
response schema/mapper semantics와 encoding variant에 binding
|
||||
- weak/strong 선택을 operation profile에 고정
|
||||
- user-private response를 shared CDN/public cache에 저장하지 않음
|
||||
- cross-origin이면 `ETag`를 expose하고 `If-None-Match`를 preflight 허용
|
||||
- 304에는 JSON success envelope를 넣지 않음
|
||||
- validator 원문을 log/metric/diagnostics에 넣지 않음
|
||||
- `Vary`와 `Cache-Control` owner를 명확히 하고 browser HTTP cache와
|
||||
TanStack/application revalidation이 서로 다른 value owner가 되지 않게 함
|
||||
|
||||
Frontend는 validator와 mapped cache value의 scope, query identity,
|
||||
representation version과 cache revision이 모두 일치할 때만 304를 success로
|
||||
받는다. cache value가 없으면 unconditional refetch 또는 safe failure로 닫는다.
|
||||
|
||||
### 8.2 Conditional command
|
||||
|
||||
수정/삭제 command가 선택되면 DTO에 opaque domain `revision`을 추가하고:
|
||||
|
||||
```text
|
||||
If-Match: "<revision validator>"
|
||||
```
|
||||
|
||||
를 요구한다. 일치하지 않으면 `412` 또는 승인된 `409` contract 하나만
|
||||
사용한다. frontend optimistic layer의 commit/rollback은 backend revision
|
||||
authority를 대체하지 않는다.
|
||||
|
||||
## 9. Optimistic mutation을 위한 backend 의미
|
||||
|
||||
Frontend ordered optimistic layer runtime은 구현돼 있지만 제품 operation에
|
||||
연결하려면 backend가 다음을 결정해야 한다.
|
||||
|
||||
- resource/list membership을 결정하는 canonical filter와 sort
|
||||
- command가 생성/수정/삭제하는 stable identity
|
||||
- server-assigned ID와 client correlation의 reconcile 방법
|
||||
- authoritative resource/list revision
|
||||
- conflict status와 stable error code
|
||||
- commit response가 complete resource인지 receipt인지
|
||||
- effect certainty와 idempotency status/reconcile endpoint
|
||||
- event/outbox가 있을 때 sequence/gap/snapshot reset 의미
|
||||
|
||||
Create가 server-assigned ID를 사용하는 경우 temporary UI ID를 backend ID로
|
||||
원자적으로 교체하고 관련 detail/list key를 reconcile하는 정책이 필요하다.
|
||||
이 의미 없이 generic optimistic append를 기본 활성화하지 않는다.
|
||||
|
||||
## 10. Database와 application service baseline
|
||||
|
||||
구현 예시는 다음 논리 table/constraint를 만족해야 한다.
|
||||
|
||||
```text
|
||||
reference_resource
|
||||
tenant_id
|
||||
resource_id
|
||||
display_name
|
||||
note
|
||||
revision
|
||||
created_at
|
||||
updated_at
|
||||
deleted_at | null
|
||||
unique(tenant_id, resource_id)
|
||||
|
||||
idempotency_record
|
||||
principal/tenant fingerprint
|
||||
operation + contract version
|
||||
key hash
|
||||
request fingerprint
|
||||
state + receipt
|
||||
lease/retention timestamps
|
||||
unique(principal/tenant, operation, contract version, key hash)
|
||||
|
||||
outbox_event when selected
|
||||
aggregate identity + revision
|
||||
event type/version
|
||||
sequence
|
||||
payload reference or bounded safe projection
|
||||
publication state
|
||||
```
|
||||
|
||||
Application service transaction은 authorization scope와 tenant predicate를
|
||||
모든 read/write에 적용하고, resource mutation과 revision/outbox 기록을 같은
|
||||
transaction boundary에 둔다. cache/replica lag를 고려해 command 직후 read
|
||||
consistency와 invalidation owner를 선언한다.
|
||||
|
||||
## 11. GraphQL 선택 시 추가 구조
|
||||
|
||||
GraphQL은 제품 operation이 REST보다 aggregation 이점을 실제로 가질 때만
|
||||
선택한다.
|
||||
|
||||
```text
|
||||
Browser
|
||||
-> persisted-operation endpoint
|
||||
-> manifest allowlist
|
||||
-> auth/CSRF/rate/cost/depth/alias enforcement
|
||||
-> GraphQL router
|
||||
-> application services/loaders
|
||||
```
|
||||
|
||||
Backend handoff:
|
||||
|
||||
- authenticated immutable schema artifact와 digest
|
||||
- named operation source와 persisted ID/hash manifest
|
||||
- variables/result runtime fixtures
|
||||
- selected GraphQL-over-HTTP revision과 exact media/status profile
|
||||
- partial data policy와 safe error extension vocabulary
|
||||
- field/row authorization, cost/depth/alias/list ceiling
|
||||
- N/N-1 router/frontend manifest rollout과 retirement
|
||||
|
||||
Production endpoint는 arbitrary document와 persisted miss 후 full-document
|
||||
fallback을 받지 않는다. normalized frontend entity cache는 별도 제품 선택이다.
|
||||
|
||||
## 12. gRPC-Web 선택 시 추가 구조
|
||||
|
||||
gRPC-Web은 browser-facing gateway/proxy가 실제 선택된 unary 또는 bounded
|
||||
server-stream operation에만 사용한다.
|
||||
|
||||
```text
|
||||
Browser
|
||||
-> same-origin BFF/Envoy/gRPC-Web gateway
|
||||
-> exact service/method allowlist
|
||||
-> frame/message/deadline/status/trailer enforcement
|
||||
-> internal gRPC application service
|
||||
```
|
||||
|
||||
Backend handoff:
|
||||
|
||||
- authenticated proto source와 immutable descriptor digest
|
||||
- Buf/protoc lint/breaking 및 deterministic generation evidence
|
||||
- exact service/method/rpc-kind allowlist
|
||||
- selected gRPC-Web runtime kind, client API와 binary/JSON/text/wire revision;
|
||||
official XHR와 Connect-Web Fetch profile을 분리
|
||||
- proxy CORS, content-type, terminal status/trailer behavior
|
||||
- Envoy를 선택하면 exact version/config digest, filter order, upstream HTTP/2,
|
||||
route/idle/max-stream timeout, timeout offset와 buffering/flush
|
||||
- message/frame/count/queue/idle/total budget
|
||||
- server-stream sequence, gap, resume와 snapshot reset protocol
|
||||
- actual browser/proxy conformance
|
||||
|
||||
client streaming과 bidirectional streaming은 common gRPC-Web browser contract로
|
||||
간주하지 않는다. upload는 REST transfer, duplex는 별도 protocol을 선택한다.
|
||||
|
||||
## 13. Connect-Web/Connect 선택 시 추가 구조
|
||||
|
||||
Connect는 Protobuf-first backend의 selected unary 또는 bounded server-stream
|
||||
operation에만 사용한다. Connect protocol과 Connect-Web의 gRPC-Web transport는
|
||||
서로 다른 provider row다.
|
||||
|
||||
```text
|
||||
Browser Connect-Web adapter
|
||||
-> same-origin BFF 또는 exact cross-origin Connect endpoint
|
||||
-> auth/CSRF/CORS + service/method allowlist
|
||||
-> Connect protocol handler
|
||||
-> application service
|
||||
```
|
||||
|
||||
Backend handoff:
|
||||
|
||||
- authenticated proto/Buf source, descriptor와 generated-service digest
|
||||
- exact Connect-Web/client runtime과 server/gateway version
|
||||
- protocol revision, JSON/binary encoding, POST 또는 approved GET
|
||||
- unary HTTP/error profile 또는 stream EndStream terminal profile
|
||||
- request/response/envelope/message/count/queue byte ceiling
|
||||
- unary/stream compression capability; stock browser stream은 identity-only
|
||||
- total/idle timeout, browser abort→server context→downstream cancellation 전파
|
||||
- exact CORS allow/expose/preflight와 auth/CSRF profile
|
||||
- actual Chromium/Firefox/WebKit와 selected proxy/server conformance
|
||||
|
||||
GET은 descriptor `NO_SIDE_EFFECTS`, non-sensitive bounded input, URL/cache key,
|
||||
`Vary`와 credential policy가 모두 승인된 unary에만 허용한다. browser
|
||||
client-streaming/bidi는 Connect protocol 자체 기능과 별개로 `PLATFORM_LIMITED`다.
|
||||
|
||||
## 14. Protobuf REST Gateway 선택 시 추가 구조
|
||||
|
||||
한 route는 `CURATED_BFF | GRPC_GATEWAY | ENVOY_TRANSCODER` 중 하나만 소유한다.
|
||||
현재 reference REST의 envelope와 `200|201`, 향후 `204/304/412` 의미를 유지하는
|
||||
기본 선택은 curated BFF다.
|
||||
|
||||
Direct gateway는 ProtoJSON/HttpRule/status/error 자체를 새 public contract로
|
||||
승인한 unary operation에서만 선택한다. Backend handoff:
|
||||
|
||||
- `.proto` annotation 또는 precedence가 고정된 service config의 immutable source
|
||||
- descriptor/Buf image, canonical HttpRule route manifest와 digest
|
||||
- pinned gateway/runtime/generator/plugin과 generated OpenAPI artifact
|
||||
- ProtoJSON name/default/enum/int64/bytes/null/presence/unknown-field profile
|
||||
- exact method/path/query/body/response-body/additional-binding와 path escaping
|
||||
- safe status/error/header mapping과 raw `google.rpc.Status` detail redaction
|
||||
- CORS/auth/CSRF, body/header/query ceiling와 rate limit
|
||||
- browser abort/deadline의 upstream gRPC/application work 전파
|
||||
- N/N-1 route/OpenAPI/runtime conformance와 coherent rollback
|
||||
|
||||
Gateway는 durable idempotency, pagination snapshot, ETag/HTTP conditional,
|
||||
product envelope, authorization와 file transfer semantics를 자동 구현하지 않는다.
|
||||
필요한 operation은 application service와 BFF가 계속 소유한다. generated
|
||||
REST streaming은 별도 framing/terminal/cache ADR 없이는 `NOT_SELECTED`다.
|
||||
|
||||
## 15. Invalidation과 realtime
|
||||
|
||||
현재 frontend cross-tab invalidation은 같은 browser origin 안의 opaque
|
||||
invalidate-only hint다. backend event delivery를 의미하지 않는다.
|
||||
|
||||
Backend-driven invalidation/realtime을 선택하면:
|
||||
|
||||
- transactional outbox 또는 동등한 durable publication
|
||||
- principal/tenant authorization을 통과한 event projection
|
||||
- event type/version, aggregate revision, sequence와 dedupe identity
|
||||
- reconnect cursor, gap detection과 snapshot reset
|
||||
- retention, replay ceiling과 slow-consumer policy
|
||||
|
||||
를 제공해야 한다. event payload를 authoritative resource snapshot으로 쓸지
|
||||
query invalidate hint로만 쓸지 operation별 reducer contract가 필요하다.
|
||||
|
||||
## 16. Rate limit, deadline와 retry
|
||||
|
||||
- backend deadline은 frontend total deadline보다 짧거나 cancellation을 전파할 수
|
||||
있어야 한다.
|
||||
- disconnect/cancel 뒤 불필요한 query 작업은 중단한다.
|
||||
- keyed command는 disconnect가 transaction rollback을 보장하지 않으므로
|
||||
idempotency receipt로 effect를 판정한다.
|
||||
- `Retry-After`는 selected status에서만 bounded delta/date 형식으로 제공한다.
|
||||
- retry-safe read와 keyed command를 구분한다.
|
||||
- proxy, BFF와 service retry가 겹쳐 retry amplification을 만들지 않게 한 owner만
|
||||
재시도한다.
|
||||
- rate limit key는 principal/tenant/operation과 abuse policy에 binding하며 raw
|
||||
credential/IP를 metric label에 넣지 않는다.
|
||||
|
||||
## 17. Observability와 privacy
|
||||
|
||||
허용되는 공통 dimension:
|
||||
|
||||
```text
|
||||
operation ID
|
||||
contract/profile version
|
||||
status group / safe error code
|
||||
attempt bucket
|
||||
duration bucket
|
||||
provider/runtime health
|
||||
traffic admission stage
|
||||
```
|
||||
|
||||
금지:
|
||||
|
||||
- Authorization, cookie, CSRF와 idempotency key
|
||||
- request/response body와 validation value
|
||||
- URL query, cursor, snapshot, ETag/revision
|
||||
- GraphQL variables/path/raw error/extensions
|
||||
- protobuf bytes, metadata와 trailer 원문
|
||||
- user ID/email/file name을 metric label이나 trace attribute로 사용
|
||||
|
||||
Request ID와 trace ID는 browser에 반환할 수 있지만 credential 역할을 하지 않으며
|
||||
추측 가능한 database primary key를 포함하지 않는다.
|
||||
|
||||
필수 SLO/alert 후보:
|
||||
|
||||
- operation availability와 latency
|
||||
- 401/403/409/412/422/429 및 5xx rate
|
||||
- schema/mapper/contract mismatch
|
||||
- idempotency in-progress age, collision과 unknown effect
|
||||
- cursor invalid/expired/loop-equivalent server detection
|
||||
- conditional hit/miss와 invalid 304
|
||||
- GraphQL persisted miss/cost reject
|
||||
- Connect missing/duplicate EndStream, whole-body/queue overflow와 compression mismatch
|
||||
- gRPC-Web missing terminal status, frame/idle/queue overflow
|
||||
- REST Gateway route/OpenAPI/runtime rewrite drift와 cancel propagation loss
|
||||
|
||||
## 18. 배포, compatibility와 rollback
|
||||
|
||||
권장 순서:
|
||||
|
||||
1. contract artifact와 compatibility diff를 생성한다.
|
||||
2. backend가 N/N-1 fixture를 통과한 상태로 먼저 배포한다.
|
||||
3. frontend operation은 traffic disabled 상태에서 staging conformance를 실행한다.
|
||||
4. read-only shadow/canary 뒤 query traffic을 올린다.
|
||||
5. keyed command는 idempotency/reconcile fault injection 뒤 별도 canary한다.
|
||||
6. pagination, conditional, optimistic, GraphQL, Connect, gRPC-Web과 REST
|
||||
Gateway는 각각 독립 gate로 승격한다.
|
||||
7. provider/browser/operations evidence가 완료된 operation만 enabled한다.
|
||||
|
||||
Rollback은 frontend/backend/contract artifact를 coherent set으로 되돌린다.
|
||||
unknown-effect command를 다른 protocol이나 새 idempotency key로 replay하지 않는다.
|
||||
Backend가 old contract를 제거하는 시점은 실제 frontend support window와 cache/CDN
|
||||
retention 뒤다.
|
||||
|
||||
## 19. Conformance와 fault-injection matrix
|
||||
|
||||
Backend 완료 판정에는 unit test 외에 actual staging provider evidence가 필요하다.
|
||||
|
||||
| 범위 | 필수 증거 |
|
||||
| --- | --- |
|
||||
| REST | exact path/query/body, media/status/envelope, max body, malformed/truncated JSON |
|
||||
| Auth | missing/expired credential, 401/403, rotation, cross-origin preflight |
|
||||
| Command | concurrent same-key replay, fingerprint mismatch, commit 뒤 response loss |
|
||||
| Cursor | filter/sort binding, expiry, snapshot stability, loop/gap/duplicate 방지 |
|
||||
| Conditional | 200→304, cache-missing 304 방지, representation change, 412 |
|
||||
| Schema | additive/breaking/null/enum/time/number fixtures와 N/N-1 |
|
||||
| GraphQL | persisted hit/miss/hash mismatch, partial, cost/depth, router rollout |
|
||||
| Connect | JSON/binary unary, GET restriction, EndStream, body/message cap, compression, cancel/deadline와 CORS |
|
||||
| gRPC-Web | proxy media/status/trailer, oversized frame, cancel, idle, gap/resume |
|
||||
| REST Gateway | HttpRule path/query/body, ProtoJSON, OpenAPI/status/error rewrite, abort propagation과 N/N-1 |
|
||||
| Operations | deadline/retry amplification, rate limit, kill switch, coherent rollback |
|
||||
|
||||
## 20. Backend handoff checklist
|
||||
|
||||
- [ ] Browser-facing API/BFF owner와 on-call이 정해졌다.
|
||||
- [ ] reference REST exact endpoint/envelope/status/media fixture가 있다.
|
||||
- [ ] bearer 또는 cookie+CSRF 중 하나의 실제 profile과 CORS evidence가 있다.
|
||||
- [ ] stable error vocabulary와 redaction contract가 있다.
|
||||
- [ ] keyed command idempotency store, TTL, receipt와 reconcile이 있다.
|
||||
- [ ] CursorPage를 선택했다면 opaque cursor/snapshot contract가 있다.
|
||||
- [ ] conditional을 선택했다면 ETag/304/412와 cache owner가 있다.
|
||||
- [ ] optimistic을 선택했다면 identity/membership/revision/conflict 의미가 있다.
|
||||
- [ ] OpenAPI/runtime schema/mapper semantic artifact와 digest가 release에 binding됐다.
|
||||
- [ ] GraphQL을 선택했다면 schema/persisted manifest/router evidence가 있다.
|
||||
- [ ] Connect를 선택했다면 descriptor/runtime/server/browser evidence가 있다.
|
||||
- [ ] gRPC-Web을 선택했다면 descriptor/proxy/browser evidence가 있다.
|
||||
- [ ] REST Gateway를 선택했다면 kind/HttpRule/ProtoJSON/OpenAPI와 runtime
|
||||
conformance evidence가 있다.
|
||||
- [ ] staging conformance, fault injection, canary, kill switch와 rollback drill이
|
||||
통과했다.
|
||||
|
||||
## 21. Frontend 완료 경계
|
||||
|
||||
Backend 구현과 별개로 현재 frontend 상태를 다음처럼 해석한다.
|
||||
|
||||
| 범위 | 현재 상태 | 남은 owner |
|
||||
| --- | --- | --- |
|
||||
| REST path/provider/auth/deadline/bounded JSON | `COMPOSED` | actual provider conformance는 backend/operations |
|
||||
| runtime schema와 mapper registry | `COMPOSED` | artifact digest/source provenance는 backend contract source + frontend/platform |
|
||||
| session generation과 query identity | `COMPOSED` | account identity projection은 identity integration + frontend |
|
||||
| Cursor runtime | `AVAILABLE_NOT_COMPOSED` | CursorPage backend 계약 후 frontend query binding |
|
||||
| conditional validator store | `AVAILABLE_NOT_COMPOSED` | ETag/304/412 backend 계약 후 frontend HTTP/cache transaction |
|
||||
| ordered optimistic layer | `AVAILABLE_NOT_COMPOSED` | product membership/revision 승인 후 frontend mutation definition |
|
||||
| GraphQL adapter | `DESIGNED_NOT_IMPLEMENTED` | product/backend 선택 뒤 frontend adapter/codegen |
|
||||
| Browser RPC V3 공통 계약/coordinator | `AVAILABLE_NOT_COMPOSED` | selected descriptor/generated client와 protocol transport 확정 뒤 frontend provider adapter |
|
||||
| Protobuf schema/codegen | `DESIGNED_NOT_IMPLEMENTED` | authenticated backend contract source 선택 뒤 pinned frontend generation |
|
||||
| Connect-Web adapter | `DESIGNED_NOT_IMPLEMENTED` | product/backend/server 선택 뒤 frontend adapter/codegen |
|
||||
| gRPC-Web adapter | `DESIGNED_NOT_IMPLEMENTED` | product/backend/proxy 선택 뒤 frontend adapter/codegen |
|
||||
| Protobuf REST Gateway | `NOT_SELECTED` | gateway kind와 public HTTP contract 승인 뒤 REST adapter binding |
|
||||
| persisted query/offline command | `NOT_SELECTED` | 별도 product ADR와 backend durability 계약 |
|
||||
|
||||
따라서 “backend만 구현하면 frontend가 아무 변경 없이 모든 capability를 자동
|
||||
사용한다”는 의미는 아니다. 현재 선택된 REST reference vertical의 공통 frontend
|
||||
기반은 완료됐지만, backend contract가 확정되면 Cursor/conditional/optimistic의
|
||||
마지막 composition과 schema/mapper 변경이 frontend에 남는다. GraphQL,
|
||||
Connect/gRPC-Web과 Protobuf REST Gateway는 제품이 선택되지 않았다. 공통 Browser
|
||||
RPC operation/profile registry, application port와 lifecycle coordinator는
|
||||
구현했지만, wire별 generated client/decoder/provider binding은 아직 구현하지
|
||||
않았다. 따라서 backend contract가 정해져도 frontend provider adapter와
|
||||
composition 작업은 명시적으로 남는다.
|
||||
@@ -0,0 +1,385 @@
|
||||
# Browser data capability completion ledger
|
||||
|
||||
## 1. 목적
|
||||
|
||||
이 문서는 다음 browser data capability의 **현재 구현 상태, 목표 상태, 남은
|
||||
공통 구현, 제품 조합 책임, backend/provider 계약과 promotion 조건**을 한곳에서
|
||||
관리하는 기준 문서다.
|
||||
|
||||
- File, Blob, 파일 선택기, preview와 다운로드
|
||||
- Local/Session Storage, IndexedDB, OPFS와 Cache Storage
|
||||
- TanStack Query memory cache와 탭 간 무효화
|
||||
- Presigned URL, multipart/resumable upload와 streaming download
|
||||
- Range resumable download와 background upload/download
|
||||
- Image CDN descriptor, 검증, delivery와 presentation
|
||||
|
||||
각 상세 문서는 메커니즘과 불변조건을 설명한다. 이 ledger는 상세 문서를
|
||||
대체하지 않으며, 서로 다른 문서의 "구현됨", "사용 가능", "설계됨" 표현이
|
||||
production readiness로 잘못 합쳐지는 것을 막는 상태 단일 기준이다.
|
||||
|
||||
이 문서가 정한 상태만으로 실제 제품의 `PRODUCTION_READY`를 주장할 수 없다.
|
||||
제품 owner, backend/provider conformance와 세 browser promotion evidence가 모두
|
||||
별도 gate를 통과해야 한다.
|
||||
|
||||
## 2. 상태 체계
|
||||
|
||||
### 2.1 Primary current status
|
||||
|
||||
각 capability는 다음 다섯 상태 중 정확히 하나를 갖는다.
|
||||
|
||||
| 상태 | 의미 | 허용되는 주장 |
|
||||
| --- | --- | --- |
|
||||
| `COMPOSED` | production bootstrap 또는 설치된 feature 호출 경로에 concrete runtime이 연결돼 있다. | 저장소의 현재 제품 경로에서 실행된다. |
|
||||
| `AVAILABLE_NOT_COMPOSED` | port, policy와 reference runtime이 있으나 기본 production graph에서는 제거돼 있다. | opt-in 조합 후보가 존재한다. |
|
||||
| `DESIGNED_NOT_IMPLEMENTED` | 불변조건과 계약은 승인됐지만 해당 runtime 또는 필수 orchestration이 없다. | 설계/계약 backlog가 닫혔고 구현 backlog는 열려 있다. |
|
||||
| `NOT_SELECTED` | 가치, 비용, 보안과 운영 owner가 승인되지 않아 의도적으로 선택하지 않았다. | 누락이 아니라 미선택이다. |
|
||||
| `PLATFORM_LIMITED` | 브라우저 공통 보장이 불가능하거나 지원 범위가 제한된다. | capability probe와 fallback 안에서만 제공할 수 있다. |
|
||||
|
||||
`AVAILABLE_NOT_COMPOSED`를 `COMPOSED`로 표시하거나,
|
||||
`DESIGNED_NOT_IMPLEMENTED`를 테스트 fixture만으로 구현 완료 처리하지 않는다.
|
||||
`NOT_SELECTED` capability를 인접 runtime의 "미완성"으로 계산하지 않는다.
|
||||
|
||||
### 2.2 독립적인 canonical readiness 축
|
||||
|
||||
Primary status와 다음 네 canonical 축을 섞지 않는다. VD-15와 이 ledger를
|
||||
참조하는 운영 runbook도 축 이름과 literal을 정확히 이 표에 맞춘다.
|
||||
|
||||
| canonical 축 | 값 | 의미 |
|
||||
| --- | --- | --- |
|
||||
| `Selection` | `NOT_SELECTED`, `SELECTED`, `REMOVING` | 특정 제품이 capability를 채택했는지 여부 |
|
||||
| `TrafficAdmission` | `DISABLED`, `SHADOW`, `CANARY`, `ENABLED` | 조합된 runtime의 신규 작업 admission |
|
||||
| `RuntimeHealth` | `UNKNOWN`, `AVAILABLE`, `DEGRADED`, `UNAVAILABLE`, `INCOMPATIBLE` | 현재 runtime/provider 관측 상태 |
|
||||
| `PromotionEvidence` | `MISSING`, `PARTIAL`, `COMPLETE`, `EXPIRED` | 필요한 contract/provider/browser/operations 증거의 합성 결과 |
|
||||
|
||||
`PromotionEvidence`의 입력은 다음 component gate다. 이 값들은 새로운 readiness
|
||||
축이 아니라 합성 근거이며 evidence record와 함께 보존한다.
|
||||
|
||||
| component gate | 값 | 의미 |
|
||||
| --- | --- | --- |
|
||||
| contract | `MISSING`, `DRAFT`, `ACCEPTED` | frontend와 provider가 맞출 wire/behavior 계약 상태 |
|
||||
| provider | `NOT_REQUIRED`, `PENDING`, `CONFORMANT` | 실제 BFF, object storage, CDN 또는 hosting 증거 |
|
||||
| browser | `MISSING`, `PARTIAL`, `PROMOTABLE` | 승인 browser/device matrix의 native 증거 |
|
||||
| operations | `MISSING`, `DOCUMENTED`, `DRILLED` | 관측, kill switch, recovery와 rollback 실행 증거 |
|
||||
|
||||
projection은 다음처럼 고정한다.
|
||||
|
||||
- 필수 component artifact가 없으면 `MISSING`이다.
|
||||
- 유효한 일부 증거만 있거나 component가 terminal gate 전이면 `PARTIAL`이다.
|
||||
- contract가 `ACCEPTED`, provider가 `NOT_REQUIRED` 또는 `CONFORMANT`, browser가
|
||||
`PROMOTABLE`, operations가 `DRILLED`이고 모든 required artifact가 유효할 때만
|
||||
`COMPLETE`다.
|
||||
- 한 번 유효했던 required artifact가 정책의 freshness/expiry를 넘으면 다른
|
||||
component 값과 무관하게 `EXPIRED`다.
|
||||
|
||||
예를 들어 Image CDN reference runtime은
|
||||
`AVAILABLE_NOT_COMPOSED / Selection=NOT_SELECTED /
|
||||
TrafficAdmission=DISABLED / RuntimeHealth=UNKNOWN /
|
||||
PromotionEvidence=PARTIAL`이고 그 근거가
|
||||
`contract=ACCEPTED / provider=PENDING / browser=PARTIAL /
|
||||
operations=DOCUMENTED`일 수 있다. 이 행을 `COMPOSED`나
|
||||
`PRODUCTION_READY`로 줄여 쓰지 않는다.
|
||||
|
||||
### 2.3 가능한 구현 경로
|
||||
|
||||
다음은 제품이 아직 선택하지 않았고 reference source도 없는 capability가 거칠 수
|
||||
있는 **일반적인 경로 예시**다. 다섯 primary status를 선형 maturity로 정의하지
|
||||
않으며 모든 capability가 이 경로를 밟는 것도 아니다. 이미 reference runtime이
|
||||
있는 capability는 `AVAILABLE_NOT_COMPOSED`에서 시작할 수 있고, cross-browser
|
||||
의미가 불가능한 capability는 구현량과 무관하게 `PLATFORM_LIMITED`다.
|
||||
|
||||
```text
|
||||
NOT_SELECTED
|
||||
-> decision + owner + data classification
|
||||
-> DESIGNED_NOT_IMPLEMENTED
|
||||
-> implementation + deterministic evidence + removal evidence
|
||||
-> AVAILABLE_NOT_COMPOSED
|
||||
-> product policy + provider contract + bootstrap composition
|
||||
-> COMPOSED
|
||||
-> provider/browser/operations promotion gates
|
||||
-> product-local production approval
|
||||
```
|
||||
|
||||
`PLATFORM_LIMITED`는 위 흐름과 별도 제약이다. 지원 가능한 browser에서는
|
||||
지원 browser용 runtime 행을 별도 상태로 기록할 수 있지만, cross-browser 보장
|
||||
행의 primary status는 계속 `PLATFORM_LIMITED`다. 제품 계약은 지원 불가능한
|
||||
browser의 fallback을 동시에 선언해야 한다.
|
||||
|
||||
rollback은 상태를 거꾸로 가장하지 않는다. 신규 진입을 kill switch로 닫고,
|
||||
active operation을 drain 또는 abort하고, durable state를 정책대로 정리한 뒤
|
||||
composition과 production module을 제거한다.
|
||||
|
||||
## 3. 구현 책임 분류
|
||||
|
||||
남은 항목은 다음 네 분류 중 하나 이상을 갖는다.
|
||||
|
||||
| 분류 | owner | 설명 |
|
||||
| --- | --- | --- |
|
||||
| `COMMON_REQUIRED` | frontend platform | 제품 API 주소 없이도 구현할 수 있고 선택 capability의 안전성에 필수인 port, state machine, policy와 lifecycle |
|
||||
| `PRODUCT_COMPOSITION` | product/feature owner | dataset, account partition, UX, retention, quota priority, query/preset profile과 use-case facade |
|
||||
| `PROVIDER_CONTRACT` | backend/storage/CDN/infra owner | authorization, signing, server ledger, storage constraint, CDN preset와 conformance |
|
||||
| `OPTIONAL_CAPABILITY` | architecture + product approval | 필요성이 확인될 때 별도 threat model과 비용 승인을 거쳐 설치할 기능 |
|
||||
|
||||
`COMMON_REQUIRED`는 범용 mega-service를 뜻하지 않는다. 메커니즘은 공통이지만
|
||||
정책 값은 immutable composition snapshot으로 주입한다.
|
||||
|
||||
## 4. 현재 capability snapshot
|
||||
|
||||
### 4.1 File, Blob, picker와 download
|
||||
|
||||
| capability | 현재 상태 | 현재 보장 | 목표 또는 잔여 | 남은 책임 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| File/Blob intake | `AVAILABLE_NOT_COMPOSED` | opaque file ref, transient vault, metadata normalization, byte/type/signature policy, bounded range read, closed-result stream | native chunk가 hard maximum을 넘지 않도록 재분할하는 ceiling과 제품 profile | `COMMON_REQUIRED` + `PRODUCT_COMPOSITION` |
|
||||
| native input picker | `AVAILABLE_NOT_COMPOSED` | keyboard/focus 가능한 input baseline, multiple, same-file reselection, dismissal outcome | 제품별 copy와 workflow | `PRODUCT_COMPOSITION` |
|
||||
| enhanced open picker | `AVAILABLE_NOT_COMPOSED` | user activation과 conditional enhancement | browser matrix와 native input fallback 유지 | `PRODUCT_COMPOSITION` |
|
||||
| directory selection | `NOT_SELECTED` | 없음 | bounded traversal, relative-path policy, symlink/entry ceiling | `OPTIONAL_CAPABILITY` |
|
||||
| persistent file handle | `NOT_SELECTED` | native handle은 transient vault 밖으로 나가지 않음 | permission recovery, handle registry, retention/logout | `OPTIONAL_CAPABILITY` |
|
||||
| drag/drop·paste·capture | `NOT_SELECTED` | 공통 file capture primitive 일부만 재사용 가능 | 별도 adapter와 접근 가능한 UX | `OPTIONAL_CAPABILITY` |
|
||||
| object URL preview lease | `AVAILABLE_NOT_COMPOSED` | receipt binding, active-content denylist, byte cap, lease/revoke | 제품이 preview를 선택할 때 safety probe와 함께 조합 | `PRODUCT_COMPOSITION` |
|
||||
| local preview decode-safety probe | `DESIGNED_NOT_IMPLEMENTED` | 현재 dimension/pixel/decoded-memory/animation preflight 없음 | object URL 발급 전 static header/decode budget 검증 | `COMMON_REQUIRED` + `PRODUCT_COMPOSITION` |
|
||||
| browser-managed download | `AVAILABLE_NOT_COMPOSED` | synchronous resolver/vault seam과 `BROWSER_HANDOFF` outcome을 saved와 구분 | concrete BFF issuer/strict response와 제품 open/share/save UX | `PRODUCT_COMPOSITION` + `PROVIDER_CONTRACT` |
|
||||
| picker streaming save | `AVAILABLE_NOT_COMPOSED` | bounded stream, backpressure, integrity, close/abort truth | capability/size 기반 strategy selector | `COMMON_REQUIRED` |
|
||||
| bounded Blob download | `AVAILABLE_NOT_COMPOSED` | small generated artifact hard cap | browser별 상한과 server-generation fallback | `PRODUCT_COMPOSITION` |
|
||||
| Range resumable download | `DESIGNED_NOT_IMPLEMENTED` | 현재 one-shot download와 명시적으로 분리 | Range/If-Range/206, validator, checkpoint, seek/truncate, final integrity | `COMMON_REQUIRED` + `PROVIDER_CONTRACT` |
|
||||
| app-managed background download | `NOT_SELECTED` | browser-managed handoff만 존재 | 지원 browser의 progressive enhancement로만 평가 | `OPTIONAL_CAPABILITY` |
|
||||
| cross-browser app-managed background download guarantee | `PLATFORM_LIMITED` | 장시간 worker/picker/file permission 유지가 공통 보장되지 않음 | browser-managed handoff 또는 explicit unsupported fallback | 플랫폼 제약 |
|
||||
|
||||
### 4.2 Query, Web Storage와 cross-context
|
||||
|
||||
| capability | 현재 상태 | 현재 보장 | 목표 또는 잔여 | 남은 책임 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| TanStack Query memory cache | `COMPOSED` | concrete QueryClient, cancellation, stale UI, optimistic rollback, invalidate | session/account scope lifecycle, late-result fence, strict query policy/key codec | `COMMON_REQUIRED` + `PRODUCT_COMPOSITION` |
|
||||
| Local Storage registry | `COMPOSED` | 등록 key, closed codec/envelope, TTL, global hard cap, memory fallback | key별 cap, partition/logout, migration, explicit outcome, bounded sweep | `COMMON_REQUIRED` + `PRODUCT_COMPOSITION` |
|
||||
| Session Storage registry | `COMPOSED` | tab-scoped 등록 control record와 동일 codec | key별 cap과 explicit durability/outcome | `COMMON_REQUIRED` |
|
||||
| cross-tab invalidation | `COMPOSED` | invalidate-only, versioned envelope, duplicate/stale/gap 처리, BroadcastChannel→localStorage→local-only | account epoch, exact storage source, production coordinator browser E2E | `COMMON_REQUIRED` |
|
||||
| IndexedDB query persistence reference runtime | `DESIGNED_NOT_IMPLEMENTED` | persistence key는 disabled로 강제되고 persister source는 없음 | 승인 query만 dehydrate/hydrate하는 facade | 선택 시 `COMMON_REQUIRED` |
|
||||
| product query persistence | `NOT_SELECTED` | persist 대상 query, owner와 retention 승인이 없음 | reference runtime 구현 뒤 별도 opt-in | `OPTIONAL_CAPABILITY` |
|
||||
| durable cache namespace epoch | `DESIGNED_NOT_IMPLEMENTED` | release epoch만 존재 | persisted resurrection 방지 transaction ledger | persistence 선택 시 `COMMON_REQUIRED` |
|
||||
| offline mutation command queue | `NOT_SELECTED` | foreground optimistic mutation만 존재 | idempotent durable command/sync protocol | `OPTIONAL_CAPABILITY` + `PROVIDER_CONTRACT` |
|
||||
| SSR hydration | `NOT_SELECTED` | 현재 client SPA | request-scoped QueryClient와 precedence | SSR 선택 시 `PRODUCT_COMPOSITION` |
|
||||
|
||||
### 4.3 IndexedDB, OPFS와 Cache Storage
|
||||
|
||||
| capability | 현재 상태 | 현재 보장 | 목표 또는 잔여 | 남은 책임 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| generic IndexedDB runtime | `AVAILABLE_NOT_COMPOSED` | transaction-complete, CAS, idempotency, logical budget, TTL, lifecycle authority, additive DDL, resumable codec migration, blocked/versionchange | feature dataset repository/schema/codec/query와 production composition | `PRODUCT_COMPOSITION` |
|
||||
| OPFS byte runtime | `AVAILABLE_NOT_COMPOSED` | DedicatedWorker SyncAccessHandle, async fallback, Web Locks, hash tree, IDB journal saga, budget/GC/reconcile | 제품 namespace/dataset policy와 production composition | `PRODUCT_COMPOSITION` |
|
||||
| OPFS real readiness preflight | `DESIGNED_NOT_IMPLEMENTED` | 없음; API property probe와 별도 native conformance test만 존재 | worker/lock/journal/small write-read-delete-cleanup을 한 readiness operation으로 검증 | `COMMON_REQUIRED` |
|
||||
| OPFS physical/journal forward migration | `DESIGNED_NOT_IMPLEMENTED` | 없음; 현재 v1 layout/journal과 reconciliation만 존재 | copy-on-write generation, checkpoint, publish authority와 N-1 rollback | `COMMON_REQUIRED` |
|
||||
| public static Cache release runtime | `AVAILABLE_NOT_COMPOSED` | same-origin public GET, exact Vary/URL/type/size/digest, stage/activate/previous rollback | 제품 release/hosting policy와 production composition | `PRODUCT_COMPOSITION` |
|
||||
| bounded Cache inspect/cleanup | `DESIGNED_NOT_IMPLEMENTED` | 없음; 현재 ownership 검사는 지키지만 cache-count scan은 unbounded | policy/epoch-bound cursor, count/deadline과 partial-success resume | `COMMON_REQUIRED` |
|
||||
| Cache control/prefix forward migration | `DESIGNED_NOT_IMPLEMENTED` | 없음; 현재 v1 control/prefix parser와 release primitive만 존재 | 새 schema candidate, verify/activate, N-1 retain과 bounded cleanup | `COMMON_REQUIRED` |
|
||||
| cross-store quota lifecycle | `DESIGNED_NOT_IMPLEMENTED` | store별 logical budget과 StorageManager signal은 존재 | write admission, pressure hysteresis, GC priority, one retry, scheduled maintenance | `COMMON_REQUIRED` + `PRODUCT_COMPOSITION` |
|
||||
| Service Worker offline fetch | `NOT_SELECTED` | Cache Storage runtime은 window에서도 독립 사용 가능 | registration, install/waiting/activation, client drain, navigation strategy | `OPTIONAL_CAPABILITY` |
|
||||
| private response cache | `NOT_SELECTED` | 현재 public cache가 명시적으로 거절 | 별도 partition/encryption 오해 방지/retention threat model | `OPTIONAL_CAPABILITY` + `PROVIDER_CONTRACT` |
|
||||
| sparse Range cache | `NOT_SELECTED` | `Range` request와 206 response를 거절 | validator-bound sparse segment merge | `OPTIONAL_CAPABILITY` + `PROVIDER_CONTRACT` |
|
||||
|
||||
### 4.4 Presigned transfer, upload와 Image CDN
|
||||
|
||||
| capability | 현재 상태 | 현재 보장 | 목표 또는 잔여 | 남은 책임 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| presigned capability | `AVAILABLE_NOT_COMPOSED` | fixed endpoint provider, strict binding, in-memory single-use vault, safe data-plane fetch | explicit download wire version, browser-handoff provider, actual signer conformance | `COMMON_REQUIRED` + `PROVIDER_CONTRACT` |
|
||||
| multipart/resumable upload | `AVAILABLE_NOT_COMPOSED` | part hash/retry, IDB checkpoint, server reconcile, cross-tab cancel, complete/abort | pause, checkpoint inventory/retention sweep, unsupported lock decision와 실제 server/session provider | `COMMON_REQUIRED` + `PRODUCT_COMPOSITION` + `PROVIDER_CONTRACT` |
|
||||
| one-shot streaming download | `AVAILABLE_NOT_COMPOSED` | bounded whole-object stream, length/media/integrity, picker/Blob/handoff delivery | strategy selector와 Range capability 분리 | `COMMON_REQUIRED` |
|
||||
| top-level transfer composition | `DESIGNED_NOT_IMPLEMENTED` | 개별 factory와 dispose는 존재 | strict config, readiness, atomic account teardown, drain, kill switch | `COMMON_REQUIRED` |
|
||||
| Image CDN verification engine | `AVAILABLE_NOT_COMPOSED` | opaque asset/preset, signed descriptor verification, responsive candidate, static metadata/decode budget | 제품 preset/presentation policy 조합과 실제 provider/private delivery E2E | `PRODUCT_COMPOSITION` + `PROVIDER_CONTRACT` |
|
||||
| image descriptor HTTP provider | `DESIGNED_NOT_IMPLEMENTED` | caller가 decoded descriptor를 직접 제공 | fixed BFF endpoint, bounded schema, refresh single-flight, expiry/logout fence | `COMMON_REQUIRED` + `PROVIDER_CONTRACT` |
|
||||
| safe image presentation primitive | `DESIGNED_NOT_IMPLEMENTED` | descriptor 결과만 제공 | URL 재조립 없는 picture/source/img projection | `COMMON_REQUIRED` + `PRODUCT_COMPOSITION` |
|
||||
| app-managed background upload | `NOT_SELECTED` | checkpoint 기반 foreground resume만 존재 | worker lifetime/staging/permission 모델 별도 설계 | `OPTIONAL_CAPABILITY` |
|
||||
| cross-browser app-managed background upload guarantee | `PLATFORM_LIMITED` | page/worker lifetime과 local source permission이 공통 보장되지 않음 | foreground resume 또는 explicit unsupported fallback | 플랫폼 제약 |
|
||||
|
||||
## 5. 중요한 경계
|
||||
|
||||
### 5.1 같은 이름처럼 보이지만 다른 capability
|
||||
|
||||
- streaming download는 메모리 상한을 지키며 **이번 응답을 끝까지** 저장한다.
|
||||
Range resumable download는 새로운 요청에서 validator와 destination offset을
|
||||
검증해 **이전 partial state를 이어 간다**.
|
||||
- multipart resume는 upload session protocol이다. Background upload는 page
|
||||
lifecycle이 끝난 뒤에도 실행 주체가 살아 있다는 별도 보장이다.
|
||||
- Cache Storage release runtime은 public response를 검증·활성화한다. Service
|
||||
Worker는 navigation/fetch interception과 controlled-client lifecycle을 소유한다.
|
||||
- generic IndexedDB runtime은 query persistence가 아니다. Query persistence는
|
||||
query classification, dehydration, scope epoch와 restore precedence를 추가로
|
||||
요구한다.
|
||||
- browser-managed handoff는 브라우저에 전달했다는 결과다. application이
|
||||
저장 완료, 진행률 또는 background retry를 증명한 결과가 아니다.
|
||||
- Image CDN engine은 descriptor를 검증한다. BFF descriptor 발급, 실제 CDN,
|
||||
`<picture>` UX를 자동으로 제공하지 않는다.
|
||||
|
||||
### 5.2 정책과 도메인
|
||||
|
||||
byte ceiling, retry 상한, schema version, state transition과 fail-closed fallback은
|
||||
공통 메커니즘이다. 다음 값은 도메인 코드가 아니라 **제품 composition policy**다.
|
||||
|
||||
- 어떤 file purpose와 MIME/signature profile을 허용하는가
|
||||
- 어떤 query/dataset을 어느 account partition에 얼마나 오래 저장하는가
|
||||
- quota pressure에서 무엇을 먼저 제거하는가
|
||||
- 어떤 upload purpose와 CDN preset을 설치하는가
|
||||
- save/open/share와 conflict/recovery UX를 어떻게 보여 주는가
|
||||
|
||||
업무 entity와 권한 결과는 backend/domain이 소유한다. frontend policy는 이를
|
||||
추측하거나 대체하지 않는다.
|
||||
|
||||
## 6. External authority·backend·provider 계약
|
||||
|
||||
server/session/CDN 경계를 넘는 capability는 해당되는 external 계약 없이 실제
|
||||
제품에 조합하지 않는다. local-only/reconstructable dataset에 backend를
|
||||
일괄 요구하지 않는다.
|
||||
|
||||
| 경계 | 맞춰야 하는 owner/authority/provider 계약 |
|
||||
| --- | --- |
|
||||
| File upload | Web/BFF authorization, upload session API, file server 또는 object-storage data plane, quarantine/scanner/promotion |
|
||||
| Presigned URL | BFF signer, cloud object storage, CORS/CSP, method/header/length/checksum/expiry 강제 |
|
||||
| Multipart resume | server session ledger, idempotency, authoritative part status, completion receipt와 orphan janitor |
|
||||
| Range download | immutable object generation 또는 strong validator, exact Range/If-Range semantics, full-object digest |
|
||||
| account cache scope | frontend common runtime은 scope snapshot 검증, local generation/fence/teardown을 소유한다. product composition은 account/tenant 의미를 opaque partition policy에 mapping하고, auth/session owner는 sign-in/revoke/switch 사실을 제공한다. backend-issued epoch를 선택한 경우에만 그것이 wire 계약이다. |
|
||||
| offline mutation | idempotency key, entity revision/ETag, cursor/delta, conflict/merge protocol |
|
||||
| Image CDN | BFF descriptor endpoint, asset revision/preset registry, signing key rotation, CDN cache/CORS/CSP/no-store |
|
||||
| eviction recovery | server-authoritative projection의 재구성 또는 all-marker-loss 구분이 필요한 제품에만 re-sync cursor/opaque installation epoch 계약 |
|
||||
|
||||
브라우저 native `File`, `FileSystemHandle`, IndexedDB physical store, OPFS path,
|
||||
Cache name과 local checkpoint revision은 backend wire 계약이 아니다.
|
||||
|
||||
## 7. 설계 우선 work package
|
||||
|
||||
### WP-01. Scope-safe client cache
|
||||
|
||||
- session/account/release scope snapshot과 generation
|
||||
- old QueryClient cancel, fence, clear, dispose와 remount
|
||||
- late-result rejection
|
||||
- strict query registry/key codec와 per-query ceiling
|
||||
- Web Storage per-key policy, partition/logout/migration/outcome
|
||||
- production coordinator까지 연결한 multi-page browser evidence
|
||||
|
||||
Exit: account A의 cache, storage event와 늦은 async result가 account B runtime에
|
||||
관측되거나 기록될 수 없음을 deterministic fault와 native browser test로 증명한다.
|
||||
|
||||
### WP-02. Range resumable download
|
||||
|
||||
- 별도 `ResumableDownloadPort`
|
||||
- validator-bound checkpoint와 non-authorizing persistence
|
||||
- 200/206/412/416 state machine
|
||||
- seek/truncate 또는 OPFS staging destination
|
||||
- capability renewal와 final whole-object integrity
|
||||
- browser strategy selector와 fail-closed fallback
|
||||
|
||||
Exit: crash, capability expiry, object replacement, malformed Content-Range,
|
||||
destination mismatch와 integrity failure에서 corrupt saved outcome이 0건이다.
|
||||
|
||||
### WP-03. Origin storage lifecycle
|
||||
|
||||
- StorageManager signal + actual quota failure 기반 pressure controller
|
||||
- policy-owned eviction priority와 hysteresis
|
||||
- bounded maintenance cursor/deadline
|
||||
- IDB/OPFS/Cache forward migration과 N-1 rollback
|
||||
- OPFS native preflight와 clear/eviction recovery
|
||||
- local preview bounded header parser/decode probe, pixel/decoded-byte/animation ceiling과
|
||||
object URL 발급 전 fail-closed rejection
|
||||
|
||||
Exit: quota/migration/crash fault에서 unbounded scan, destructive auto-reset 또는
|
||||
cross-scope read 없이 read-only/online-only/recovery outcome으로 닫힌다. hostile,
|
||||
oversize 또는 animated preview fixture는 object URL 발급 전에 거절되고 decode
|
||||
resource와 lease가 남지 않는다.
|
||||
|
||||
### WP-04. Transfer operational composition
|
||||
|
||||
- strict config schema와 protocol/version registry
|
||||
- file/presigned/upload/download/image runtime atomic assembly
|
||||
- readiness, kill switch, active-operation drain과 idempotent close
|
||||
- logout/account switch fence
|
||||
- checkpoint inventory/retention owner와 safe observations
|
||||
- frontend provider contract harness
|
||||
|
||||
Exit: partially configured runtime이 시작되지 않고, teardown 뒤 capability나
|
||||
late refresh가 새 scope에서 재사용되지 않는다.
|
||||
|
||||
### WP-05. Image descriptor delivery
|
||||
|
||||
- fixed BFF provider와 bounded closed decoder
|
||||
- descriptor refresh single-flight와 expiry budget
|
||||
- logout/account/runtime-generation fence
|
||||
- static safe picture projection
|
||||
- actual private/public CDN conformance and browser evidence
|
||||
|
||||
Exit: caller-provided URL/transform이 DOM에 도달하지 않고, 만료·회전·logout·decode
|
||||
failure가 placeholder 또는 closed failure로 복구된다.
|
||||
|
||||
### WP-06. Optional capability decisions
|
||||
|
||||
directory/persistent handle, Query persistence, offline mutation, Service Worker,
|
||||
private/range cache와 app-managed background upload/download는 각각 독립 ADR,
|
||||
threat model, owner, budget과 removal plan을 승인한 뒤에만 시작한다.
|
||||
|
||||
## 8. 문서 우선 gate
|
||||
|
||||
runtime 구현을 시작하기 전에 해당 work package 문서에 다음이 모두 있어야 한다.
|
||||
|
||||
- current/target status와 out-of-scope
|
||||
- application port와 adapter/provider owner
|
||||
- immutable policy/config schema와 implementation ceiling
|
||||
- state machine, concurrency와 cancellation owner
|
||||
- durable record 분류, scope, TTL, purge와 migration
|
||||
- backend/provider wire version과 compatibility
|
||||
- browser capability matrix와 fallback
|
||||
- observability allowlist와 금지 값
|
||||
- rollout, kill switch, rollback과 removal
|
||||
- deterministic, contract, native browser, fault와 operational drill
|
||||
- 완료 조건과 promotion evidence 위치
|
||||
|
||||
문서가 없는 편의 API, fallback, persistence field 또는 retry owner를 구현 중에
|
||||
추가하지 않는다. 새 요구는 ledger와 해당 ADR을 먼저 변경한다.
|
||||
|
||||
## 9. 구현 및 promotion 순서
|
||||
|
||||
```text
|
||||
ledger/ADR accepted
|
||||
-> port + closed policy/schema
|
||||
-> deterministic fake/contract harness
|
||||
-> reference runtime + negative boundary gate
|
||||
-> fault/migration/removal evidence
|
||||
-> AVAILABLE_NOT_COMPOSED
|
||||
-> product owner + 필요한 external provider/config 선택
|
||||
-> bootstrap composition behind kill switch
|
||||
-> COMPOSED + TrafficAdmission=DISABLED
|
||||
-> native Chromium/Firefox/WebKit + device drill
|
||||
-> provider/browser/operations promotion gates
|
||||
-> TrafficAdmission=CANARY/ENABLED
|
||||
-> project-local production promotion
|
||||
```
|
||||
|
||||
추천 구현 순서는 WP-01 → WP-02 → WP-03 → WP-04 → WP-05다. WP-02와 WP-03의
|
||||
seekable/staging 정책, WP-04와 WP-05의 lifecycle/config 계약은 설계 단계에서
|
||||
서로 검토하되 한 변경에서 모든 runtime을 동시에 조합하지 않는다.
|
||||
|
||||
## 10. 공통 완료 기준
|
||||
|
||||
- [ ] 모든 capability가 이 문서의 primary status 하나를 가진다.
|
||||
- [ ] `AVAILABLE_NOT_COMPOSED` source가 기본 production module inventory에 없다.
|
||||
- [ ] `COMPOSED` capability는 bootstrap부터 실제 consumer까지 호출 증거가 있다.
|
||||
- [ ] account/session 전환이 broadcast delivery나 브라우저 종료에 의존하지 않는다.
|
||||
- [ ] byte, record, queue, candidate, retry, deadline과 scan에 hard ceiling이 있다.
|
||||
- [ ] durable state는 schema/codec/scope/epoch/retention/migration을 함께 선언한다.
|
||||
- [ ] raw URL, query, signed header, file name/path, account ID, storage value,
|
||||
digest/ETag/receipt가 diagnostics나 telemetry에 노출되지 않는다.
|
||||
- [ ] backend/provider contract는 fake, emulator와 실제 provider에 재사용 가능한
|
||||
conformance suite를 가진다.
|
||||
- [ ] Chromium/Firefox/WebKit과 승인 device fallback 증거가 보존된다.
|
||||
- [ ] kill switch, N-1 rollback, recovery와 optional runtime removal drill이
|
||||
통과한다.
|
||||
- [ ] 외부 증거가 없는 항목을 `PRODUCTION_READY`로 표시하지 않는다.
|
||||
|
||||
## 11. 상세 문서
|
||||
|
||||
- [Browser file and origin storage](./browser-file-and-origin-storage.md)
|
||||
- [Client cache and storage](./client-cache-and-storage.md)
|
||||
- [Presigned transfer and Image CDN](./presigned-transfer-and-image-cdn.md)
|
||||
- [Server file capability infrastructure](./server-file-capability-infrastructure.md)
|
||||
- [VD-11 Browser file and origin-storage](./decisions/VD-11-browser-file-and-origin-storage.md)
|
||||
- [VD-12 Presigned transfer and Image CDN](./decisions/VD-12-presigned-transfer-and-image-cdn.md)
|
||||
- [VD-13 Client cache scope and persistence](./decisions/VD-13-client-cache-scope-and-persistence.md)
|
||||
- [VD-14 Resumable download and background download](./decisions/VD-14-resumable-download-and-background-transfer.md)
|
||||
- [VD-15 Origin storage lifecycle and migration](./decisions/VD-15-origin-storage-lifecycle-and-migration.md)
|
||||
- [VD-16 Browser transfer composition and image delivery](./decisions/VD-16-browser-transfer-composition-and-image-delivery.md)
|
||||
- [Browser file/storage recovery](../operations/browser-file-storage-recovery.md)
|
||||
- [Client cache/storage recovery](../operations/client-cache-and-storage-recovery.md)
|
||||
- [Browser transfer recovery](../operations/browser-transfer-recovery.md)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -57,17 +57,19 @@ P0/P1 acceptance와 P2 recipe 기본값은 `LOCAL_TEMPLATE_READY`다. 다만 실
|
||||
| 영역 | 현재 판정 | 근거 | 필요한 다음 상태 |
|
||||
| --- | --- | --- | --- |
|
||||
| 부트·런타임 설정 | 준비됨 | `src/bootstrap`, runtime schema, release 검사 | 현 상태 유지, TS 전환 시 동일 게이트 유지 |
|
||||
| 계층 의존 방향 | 부분 준비 | `.dependency-cruiser.cjs`, `src/application/ports` | inbound/outbound 명명과 `contracts` 소유권까지 집행 |
|
||||
| application facade | 준비됨 | typed input/output catalog, provider, production composition test | feature input use case를 contribution으로 확장 |
|
||||
| HTTP client | 준비됨 | path/search/body projection, runtime timeout/retry, abort/cleanup test | feature gateway 뒤에서 사용 |
|
||||
| retry | 준비됨 | HTTP 단일 소유, runtime max attempts, Query retry off, logical execution당 bounded diagnostics | terminal event 중복 방지 계약 유지 |
|
||||
| 오류 모델 | 부분 준비 | error registry와 normalization 존재 | typed discriminated union과 계층별 mapper |
|
||||
| 검증 | 준비됨 | runtime/API/route/form Zod parse 결과를 실행 경계에서 사용하고 domain invariant와 분리 | feature별 schema 소유권 유지 |
|
||||
| 계층 의존 방향 | 준비됨 | dependency-cruiser + TS-aware static graph, unresolved/parse/layer/cycle negative fixture | 새 rule shape와 source extension도 같은 fail-closed graph에 추가 |
|
||||
| application facade | 준비됨 | module-augmented feature input registry, typed output catalog, provider, production composition test | feature별 input contribution과 제거 gate 유지 |
|
||||
| REST HTTP client | 준비됨 + hardening delta | reference vertical의 path/search/body projection, attempt timeout/retry, abort/cleanup은 `COMPOSED` | auth fail-close/final invariant, total deadline, bounded decoder, status/media/CSRF/conditional/pagination은 `DESIGNED_NOT_IMPLEMENTED` |
|
||||
| Browser RPC 공통 계약/runtime | 준비됨/미조립 | V3 operation/profile registry, typed application port, bounded unary/server-stream lifecycle와 unavailable adapter는 `AVAILABLE_NOT_COMPOSED` | actual descriptor/generated client, protocol transport, proxy/provider/browser conformance |
|
||||
| GraphQL·Connect·gRPC-Web·Protobuf REST Gateway 제품 adapter | 설계됨/제품 미선택 | wire dependency/generated source/provider는 없고 VD-26/VD-27/VD-29/VD-30 production contract 승인 | 제품 operation/provider 선택 전 `NOT_SELECTED`; 선택 branch wire adapter 구현 뒤에만 `AVAILABLE_NOT_COMPOSED` |
|
||||
| retry | 준비됨 + hardening delta | REST 단일 소유, runtime max attempts, Query retry off, logical terminal diagnostics | total elapsed/sleep, 401 single-flight와 protocol별 exact retry/effect certainty 구현 |
|
||||
| 오류 모델 | 준비됨 | registry-derived `AppFailure`, 공통 `Result`, HTTP normalization과 invalid-kind fixture | 새 failure kind는 registry·copy·telemetry 계약과 함께 추가 |
|
||||
| Schema·Mapper | reference 준비됨 + governance delta | reference Zod → mapper → domain/application path는 `COMPOSED` | typed codec/mapper proof, semantic fingerprint/provenance, bounded decode와 generated drift gate는 `DESIGNED_NOT_IMPLEMENTED` |
|
||||
| 인증 연동 | 준비됨/프로젝트 선택 | opaque auth owner와 demo seam 존재 | 인증 방식별 recipe; 기본 token 저장소는 추가하지 않음 |
|
||||
| 서버 상태 | 준비됨 | reference route의 query/mutation, cancellation, stale, optimistic/conflict/rollback | feature별 query contribution recipe 유지 |
|
||||
| 서버 상태 | 기본 경로 준비됨 + lifecycle delta | reference query/mutation, cancellation, stale, basic optimistic/conflict/rollback은 `COMPOSED` | strict bound query policy/key, result/page ceiling, mutation concurrency/CAS rollback은 `DESIGNED_NOT_IMPLEMENTED` |
|
||||
| 클라이언트 상태 | 준비됨/프로젝트 선택 | local/URL/query/context 소유권, session external store, typed workflow recipe | 실제 cross-page workflow가 생길 때 하나의 store 선택 |
|
||||
| 범용 global store | 프로젝트 선택 | runtime library 없음, typed facade/fake와 server-state duplication gate | VD-10 조건에 따라 Zustand/Redux Toolkit/state machine 중 하나 선택 |
|
||||
| 라우팅 | 준비됨 | Data Router, typed runtime map, codec, metadata consumer, bounded chunk recovery | reference feature route와 release E2E로 사용 범위 확장 |
|
||||
| 라우팅 | 준비됨 | Data Router, typed runtime map, codec, 분리된 route-input provider, metadata consumer, bounded chunk recovery | 새 lazy route도 router 역참조 없이 contribution으로 추가 |
|
||||
| 앱 셸·반응형 | 준비됨 | native modal Drawer, compact/desktop layout, Escape/link dismiss/focus restore, pseudo reflow와 RTL direction | compact browser matrix 유지 |
|
||||
| 페이지 템플릿 | 준비됨 | Standard/Collection/Detail/Form/Status와 public design-system entry | feature별 slot 조합 유지 |
|
||||
| 디자인 토큰 | 준비됨 | primitive/semantic/component CSS, 48-token 자동 계약, dark/forced-colors/reduced-motion | 제품 brand token은 외부 프로젝트에서 확장 |
|
||||
@@ -83,7 +85,15 @@ P0/P1 acceptance와 P2 recipe 기본값은 `LOCAL_TEMPLATE_READY`다. 다만 실
|
||||
| 샘플 제거 | 준비됨 | feature/catalog/test 제거 후 type/architecture/registry/test/home/build 9단계 검증 | 새 contribution도 같은 제거 gate에 포함 |
|
||||
| registry·compatibility 집행 | 준비됨 | 10개 registry type/reference/consumer/orphan, 승인 digest와 actual semantic diff, breaking evidence | public 계약 변경 시 baseline review 유지 |
|
||||
| 공급망 검사 | 준비됨/프로젝트 선택 | 561개 transitive inventory/integrity/license, actual diff, CycloneDX, local provenance, secret/reproducible build gate | 실제 vulnerability scanner와 signed attestation 없이는 promotion `FAIL_UNVERIFIED` |
|
||||
| realtime·offline·file 등 | 준비됨/프로젝트 선택 | 12개 opt-in TypeScript port/fake/unavailable, failure/security/bundle/removal gate | 실제 요구·owner 승인 시 해당 recipe만 설치 |
|
||||
| realtime delivery | 참조 런타임 준비됨/프로젝트 미선택 | RT-01~04 공통 event authority, fetch-stream SSE, closed WebSocket, bounded Polling, Web Push window/worker와 handoff/reconnect 조정자는 `AVAILABLE_NOT_COMPOSED`; backend/provider/browser evidence는 없음 | [Realtime events, Web Push, and bounded polling](./realtime-events-web-push-and-bounded-polling.md)의 RT-05 제품 조립·운영 증거를 capability별로 통과 |
|
||||
| offline·file·browser data | 부분 준비/프로젝트 선택 | opt-in recipe와 gate; capability별 구현·조합·미구현·미선택·platform 제한 상태가 서로 다름 | [Browser data capability completion ledger](./browser-data-capability-completion-ledger.md)의 상태와 work package를 통과한 capability만 설치 |
|
||||
|
||||
REST/GraphQL/Connect/gRPC-Web/REST Gateway, Schema/Mapper와 Server State의 상세 current/target 판정은
|
||||
[API contract, Schema, Mapper와 Server State](./api-contract-schema-mapper-and-server-state.md)를
|
||||
따른다. Browser Protobuf 축의 선택 기준은
|
||||
[Protobuf browser transport와 REST Gateway](./protobuf-browser-transport-and-rest-gateway.md)를
|
||||
따른다. 현재 REST reference 경로의 `COMPOSED` 판정을 GraphQL/Connect/gRPC-Web/
|
||||
Gateway 또는 REST v2 hardening 완료 증거로 재사용하지 않는다.
|
||||
|
||||
## 5. 우선순위별 발견 사항
|
||||
|
||||
@@ -91,7 +101,7 @@ P0/P1 acceptance와 P2 recipe 기본값은 `LOCAL_TEMPLATE_READY`다. 다만 실
|
||||
|
||||
#### RP-02에서 application 런타임 우회 해결
|
||||
|
||||
`src/bootstrap/composition-root.js`가 만든 typed application input API는
|
||||
`src/bootstrap/composition-root.ts`가 만든 typed application input API는
|
||||
production `ApplicationProvider`에 주입된다. raw auth, storage, telemetry와
|
||||
release port는 closure 안에 남고 UI는 session, preference, diagnostics와 runtime
|
||||
query만 사용한다.
|
||||
@@ -125,18 +135,19 @@ rollback, conflict resolution과 invalidation을 검증한다. 다른 presentati
|
||||
|
||||
#### RP-01에서 TypeScript 검사 도구 안전망 구현
|
||||
|
||||
현재 source는 모두 JS/JSX이고 `strict + allowJs + checkJs`를 사용한다. 이는 좋은
|
||||
중간 안전망이지만 다음 도구는 TS migration을 그대로 따라가지 못한다.
|
||||
초기 source는 JS/JSX와 `strict + allowJs + checkJs`를 사용했다. RP-01 후속
|
||||
migration으로 현재 product source, tests, Node scripts와 지원 config는 모두
|
||||
TS/TSX이며 `allowJs`는 꺼져 있다.
|
||||
|
||||
- ESLint의 계층·보안 glob은 JS/JSX 중심이다.
|
||||
- registry scanner는 `.ts`와 `.tsx`를 찾지 않는다.
|
||||
- registry governance 경로가 `.js` 확장자로 고정되어 있다.
|
||||
- tests는 현재 `tsconfig.json` 검사 범위에서 빠진다.
|
||||
- ESLint의 계층·보안 규칙은 product와 negative fixture의 TS/TSX를 함께 검사한다.
|
||||
- registry scanner와 governance/baseline은 `.ts`와 `.tsx` 경로를 사용한다.
|
||||
- app, Node scripts/config, tests는 분리된 strict project로 모두 검사된다.
|
||||
- architecture gate는 fixture를 포함한 실행 source의 `.js/.jsx/.mjs/.cjs`
|
||||
재유입을 거절한다.
|
||||
|
||||
따라서 파일 확장자를 먼저 바꾸면 새 TS 코드가 일부 자동 검사에서 빠질 수 있다.
|
||||
TypeScript 전환은
|
||||
[TypeScript의 JavaScript migration 가이드](https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html)
|
||||
처럼 점진적으로 진행하되, 이 저장소에서는 tooling glob과 CI를 먼저 고쳐야 한다.
|
||||
전환은 tooling glob과 CI를 먼저 고친 뒤 계약 계층부터 화면·테스트·운영 script
|
||||
순서로 완료했다. Node 24가 운영 `.ts` script를 직접 실행하고 NodeNext strict
|
||||
typecheck가 같은 경로를 검증한다.
|
||||
|
||||
#### RP-03에서 HTTP 선언과 실행의 차이 해결
|
||||
|
||||
@@ -153,7 +164,7 @@ timeout, retry, decoder, mapper 책임을 분리해야 한다. application에는
|
||||
|
||||
#### RP-04에서 route registry를 실행 계약으로 전환
|
||||
|
||||
platform route 계약과 `src/features/installed-feature-contracts.js`의 직렬화
|
||||
platform route 계약과 `src/features/installed-feature-contracts.ts`의 직렬화
|
||||
가능한 contribution을 기준으로
|
||||
`src/presentation/routes/app-router.tsx`가 Data Router route object와
|
||||
navigation을 생성한다. `route-runtime.tsx`는 lazy component의 실행 map만
|
||||
@@ -182,9 +193,11 @@ DTO/schema, mapper, route/API/query contract, query/mutation controller와 page
|
||||
|
||||
`test:sample-removal`은 임시 복제본에서 feature source/tests를 삭제하고 installed
|
||||
contract/runtime/adapter catalog를 빈 목록으로 재생성한다. 그 뒤 typecheck,
|
||||
architecture, registry, unit/integration, home smoke, build와 fixture ID 잔여
|
||||
0개를 검사한다. 설치 모드에서는 MSW를 사용한 bootstrap → router → application
|
||||
→ HTTP → schema → mapper → query cache → page 수직 테스트가 실행된다.
|
||||
architecture, registry, unit/integration, coverage, source evidence, home smoke,
|
||||
build와 fixture ID 잔여 0개를 검사한다. feature-owned coverage/evidence policy도
|
||||
함께 제거되며 generic registry 성공 경로는 공통 unit test가 유지한다. 설치
|
||||
모드에서는 MSW를 사용한 bootstrap → router → application → HTTP → schema →
|
||||
mapper → query cache → page 수직 테스트가 실행된다.
|
||||
|
||||
#### 비동기·복구 상태의 불변식이 닫혀 있지 않다
|
||||
|
||||
@@ -273,11 +286,14 @@ vendor facade, 선택 조건, 실패 정책, 테스트 fixture를 문서로 제
|
||||
|
||||
| capability | 대표 기술 | 기본 제공할 경계 | 실제 설치 조건 |
|
||||
| --- | --- | --- | --- |
|
||||
| realtime | WebSocket, SSE | subscribe/unsubscribe, reconnect, resume, heartbeat | 서버가 push event를 제공할 때 |
|
||||
| offline storage | IndexedDB | versioned repository, migration, quota failure | offline read/write가 제품 요구일 때 |
|
||||
| background/cache | Service Worker, PWA | cache ownership, update, rollback recipe | installable/offline 앱일 때 |
|
||||
| file transfer | presigned HTTP, multipart | progress, cancel, size/type validation | 업로드·대용량 다운로드가 있을 때 |
|
||||
| generated API | OpenAPI, GraphQL, gRPC-Web | generated client를 gateway 뒤에 감싸는 규칙 | 서버 계약 형식이 확정됐을 때 |
|
||||
| foreground realtime | SSE, WebSocket | closed stream/event registry, resume/gap/snapshot, bounded queue와 lifecycle | 측정된 one-way event 또는 duplex interaction 요구와 backend replay/auth owner가 있을 때 |
|
||||
| background notification | Web Push, persistent notification | permission, subscription register/revoke, Service Worker push/click와 safe route | user-visible background notification과 provider/privacy owner가 승인됐을 때 |
|
||||
| bounded polling | conditional HTTP query | visible-only single-flight lease, request/time budget와 terminal stop | relaxed freshness 또는 의미가 보존되는 stream fallback이면 충분할 때 |
|
||||
| structured offline storage | IndexedDB | feature repository, codec/schema 분리, resumable migration, revision/idempotency, blocked/quota recovery | offline record/command가 제품 요구일 때 |
|
||||
| large local binary | OPFS + IndexedDB journal | immutable chunk, generation/integrity, crash reconciliation, bounded GC | 실제 large local object와 retention owner가 있을 때 |
|
||||
| public HTTP representation cache | Cache Storage, Service Worker | auth/private 배제, exact match, candidate integrity, update/rollback | install/offline shell 또는 승인된 public cache가 필요할 때 |
|
||||
| file selection/transfer/delivery | native input, File/Blob, picker, multipart, stream save | opaque file ref, bounded inspection, upload session, handoff/save 구분, object-URL lease | backend 재검증을 포함한 file workflow가 있을 때 |
|
||||
| generated API | OpenAPI, GraphQL, Connect/gRPC-Web, Protobuf | generated client를 semantic gateway 뒤에 감싸고 transport/gateway 축을 분리하는 규칙 | 서버 계약 형식과 selected operation/provider가 확정됐을 때 |
|
||||
| feature flag | local/remote flag provider | typed flag key, default, stale behavior | staged rollout가 필요할 때 |
|
||||
| worker | Web Worker | request/result/cancel protocol | UI thread를 막는 CPU 작업이 있을 때 |
|
||||
| multi-tab | BroadcastChannel | event versioning, source ID, conflict policy | 탭 간 동기화가 필요할 때 |
|
||||
@@ -286,18 +302,46 @@ vendor facade, 선택 조건, 실패 정책, 테스트 fixture를 문서로 제
|
||||
| large data UI | virtualization, data grid | owned component facade | 데이터 규모가 측정 기준을 넘을 때 |
|
||||
| analytics/error sink | vendor SDK, OpenTelemetry | redaction, consent, sampling adapter | 운영 provider와 정책이 정해졌을 때 |
|
||||
|
||||
12개 항목의 현재 상태는 모두 `RECIPE_AVAILABLE / NOT_INSTALLED`다.
|
||||
catalog의 12개 항목 수는 유지하며 OPFS는 offline recipe의 large-object
|
||||
sub-capability, Cache Storage는 Service Worker/PWA recipe의 독립 cache policy
|
||||
sub-capability로 깊이를 보강했다. 현재 상태는 모두
|
||||
제품 선택 기준으로 `RECIPE_AVAILABLE / NOT_INSTALLED`다. 다만 File/Blob/picker/
|
||||
download, IndexedDB/OPFS/StorageManager, Cache Storage에는 실제 native API를
|
||||
호출하는 정책 주입형 reference runtime이 `AVAILABLE_NOT_COMPOSED` 상태로 있으며,
|
||||
bootstrap과 installed feature에서는 import하지 않는다.
|
||||
`config/recipes/frontend-capability-recipes.json`이 선택/금지 조건, failure,
|
||||
cleanup, security/privacy, bundle budget, fallback과 제거 절차의 SSOT이며,
|
||||
`recipes/frontend-capabilities`에 production-excluded TypeScript port와
|
||||
fake/unavailable adapter가 있다. 도입 절차는
|
||||
`docs/architecture/optional-adapter-recipes.md`를 따른다.
|
||||
fake/unavailable adapter가 있다. file/IndexedDB/OPFS/Cache의 상세한 기술 소유권,
|
||||
journal, migration, cache activation, test와 운영 복구는
|
||||
`docs/architecture/browser-file-and-origin-storage.md`와 VD-11을 따른다.
|
||||
presigned capability, multipart/resume, bounded streaming과 Image CDN은
|
||||
`docs/architecture/presigned-transfer-and-image-cdn.md`와 VD-12를 따르며, 공통
|
||||
도입 절차는 `docs/architecture/optional-adapter-recipes.md`를 따른다.
|
||||
SSE, WebSocket, Web Push와 bounded polling은
|
||||
`docs/architecture/realtime-events-web-push-and-bounded-polling.md`와 VD-28을
|
||||
따른다. copyable recipe/fake와 별개로 RT-01~04 reusable source와 deterministic
|
||||
gate는 `AVAILABLE_NOT_COMPOSED`다. foreground transport, 제품 event registry와
|
||||
Web Push provider selection은 여전히 `NOT_SELECTED`이며 production bootstrap과
|
||||
worker에는 조립하지 않는다.
|
||||
|
||||
이 capability들을 한꺼번에 "준비됨"으로 표시하지 않는다.
|
||||
[Browser data capability completion ledger](./browser-data-capability-completion-ledger.md)가
|
||||
`COMPOSED`, `AVAILABLE_NOT_COMPOSED`, `DESIGNED_NOT_IMPLEMENTED`,
|
||||
`NOT_SELECTED`, `PLATFORM_LIMITED`를 구분하는 현재 상태의 기준이다. 특히
|
||||
session/account Query lifecycle, Range resumable download, cross-store quota
|
||||
orchestration, top-level transfer composition과 Image descriptor HTTP provider는
|
||||
설계가 승인됐어도 runtime 구현 전에는 `DESIGNED_NOT_IMPLEMENTED`다. Query
|
||||
persistence, Service Worker offline fetch와 app-managed background
|
||||
upload/download는 제품이 별도로 선택하기 전에는 `NOT_SELECTED`, 그 cross-browser
|
||||
guarantee는 `PLATFORM_LIMITED` 상태를 유지한다.
|
||||
|
||||
서버의 Redis, MongoDB, PostgreSQL, MinIO를 브라우저가 직접 연결하는 구조는 기본
|
||||
frontend adapter catalog에 넣지 않는다. 브라우저는 권한 있는 backend API/BFF를
|
||||
통해 이 자원에 접근해야 한다. 프론트에서 대응되는 변화 지점은 데이터베이스
|
||||
vendor가 아니라 HTTP/GraphQL/gRPC-Web, realtime, file transfer, cache, storage,
|
||||
worker, browser capability 같은 프로토콜·런타임 capability다.
|
||||
vendor가 아니라 HTTP/GraphQL/Connect/gRPC-Web/REST Gateway, realtime, file
|
||||
transfer, cache, storage, worker, browser capability 같은 프로토콜·런타임
|
||||
capability다.
|
||||
|
||||
### 성능 최적화는 모두 adapter 문제인가
|
||||
|
||||
@@ -382,13 +426,15 @@ bootstrap → React TSX → tests 순서로 이동한다.
|
||||
|
||||
### retry, API client, logger, token manager, error, validation은 어디에 있는가
|
||||
|
||||
- retry: `src/adapters/http/retry-policy.js`, 부분 준비
|
||||
- API client: `src/adapters/http/client.js`, 부분 준비
|
||||
- retry: `src/adapters/http/retry-policy.ts`, runtime max-attempt/cleanup 계약까지 구현
|
||||
- API client: `src/adapters/http/client.ts`, path/search/body projection과
|
||||
runtime schema/mapper를 사용하며 feature gateway 뒤에서 실행
|
||||
- logger: `DiagnosticsPort`로 telemetry와 분리해 구현. closed event/level,
|
||||
allowlist와 bounded/no-op adapter 제공
|
||||
- token manager: 의도적으로 없음. opaque external auth owner가 credential을 소유
|
||||
- error: `src/contracts/errors.js`와 HTTP normalization, 부분 준비
|
||||
- validation: runtime/API Zod는 존재, route/form/domain 분리는 미완성
|
||||
- error: `src/contracts/errors.ts`의 registry-derived `AppFailure`, 공통
|
||||
`Result`와 HTTP normalization으로 구현
|
||||
- validation: runtime/API/route/form Zod와 domain invariant를 소유 경계별로 분리
|
||||
|
||||
token manager를 기본으로 추가하지 않는 이유는 token lifecycle이 인증 방식마다
|
||||
다르고 localStorage token을 일반 해법으로 만들면 보안 위험이 커지기 때문이다.
|
||||
@@ -468,6 +514,8 @@ data 같은 프로젝트별 외부 작업을 포함하지 않는다.
|
||||
|
||||
## 8. 관련 상세 문서
|
||||
|
||||
- [API contract, Schema, Mapper와 Server State](./api-contract-schema-mapper-and-server-state.md)
|
||||
- [Realtime events, Web Push, and bounded polling](./realtime-events-web-push-and-bounded-polling.md)
|
||||
- [프론트 포트·어댑터와 기능 경계](./frontend-ports-adapters-and-boundaries.md)
|
||||
- [TypeScript·상태·데이터 흐름](./typescript-state-and-data-flow.md)
|
||||
- [라우팅·페이지·재사용 패턴](./routing-pages-and-patterns.md)
|
||||
|
||||
@@ -157,19 +157,19 @@ CI gate를 통과하게 한다. 애플리케이션 전체를 일괄 변환하지
|
||||
|
||||
- root, application/source, tests용 TypeScript project/reference를 분리한다.
|
||||
- `src`, `tests`, `scripts`의 허용 확장자와 build output 제외 범위를 명시한다.
|
||||
- ESLint, dependency-cruiser, registry scanner와 architecture script의 JS-only
|
||||
glob 및 `.js` 고정 경로를 JS/TS 양쪽으로 확장한다.
|
||||
- ESLint, dependency-cruiser, registry scanner와 architecture script의 검색
|
||||
범위를 TS/TSX source와 fixture로 통일한다.
|
||||
- Vite, Vitest와 Playwright config가 TS source/test 오류를 우회하지 않게 한다.
|
||||
- 기존 JS에는 `checkJs`, 새 TS에는 strict 정책을 적용한다.
|
||||
- 기존 fixture까지 TS/TSX로 전환하고 strict 정책을 적용한다.
|
||||
- port, registry와 forbidden import용 `.ts`/`.tsx` fixture를 추가한다.
|
||||
|
||||
**자동 테스트와 negative test**
|
||||
|
||||
- JS와 TS source/test를 함께 typecheck하는 smoke
|
||||
- TS/TSX source/test를 함께 typecheck하는 smoke
|
||||
- TSX presentation의 concrete HTTP adapter import를 architecture gate가 거절
|
||||
- 잘못된 port 구현과 discriminated union 사용이 typecheck 실패
|
||||
- TS registry row의 누락 필드, 중복 ID와 unknown reference가 gate 실패
|
||||
- 기존 JS invalid-call fixture도 계속 실패
|
||||
- 변환된 TS invalid-call fixture도 계속 실패
|
||||
- `pnpm lint`, `pnpm check:types`, `pnpm check:architecture`,
|
||||
`pnpm check:registries`, `pnpm test:all`, `pnpm build`
|
||||
|
||||
@@ -177,7 +177,7 @@ CI gate를 통과하게 한다. 애플리케이션 전체를 일괄 변환하지
|
||||
|
||||
- 허용된 모든 source/test 확장자가 typecheck, lint, architecture, registry gate
|
||||
중 필요한 검색 범위에 포함된다.
|
||||
- JS 안전망이 약해지지 않은 상태에서 TS fixture가 CI에 의해 차단된다.
|
||||
- 모든 TS/TSX negative fixture가 CI에 의해 차단된다.
|
||||
- 광범위한 unchecked cast나 임시 `any`로 통과시키지 않는다.
|
||||
- production source 대량 변환 없이 독립적으로 revert할 수 있다.
|
||||
|
||||
@@ -296,6 +296,29 @@ adapter로 연결한다.
|
||||
표준 API를 제공한다.
|
||||
- `AsyncSurface`의 모든 action이 실제 command와 검증된 state transition을 가진다.
|
||||
|
||||
**구현 증거 (2026-07-28)**
|
||||
|
||||
- installed `QUERY_REGISTRY`가 namespace와 별도의 versioned invalidation topic,
|
||||
`crossContext: "invalidate-only"`와 `persistence: "disabled"`를 소유한다.
|
||||
- `tanstack-cache-coordinator.ts`가 mutation lease, local active-query
|
||||
invalidation, remote hint coalescing, sequence-gap 전체 reconciliation과
|
||||
idempotent dispose를 구현한다.
|
||||
- production bootstrap/provider tree가 coordinator를 실제 조립한다.
|
||||
native `BroadcastChannel`을 우선하고 실패하면 exact localStorage pulse로
|
||||
fallback하며 둘 다 없으면 local-only로 degrade한다.
|
||||
- wire contract는 2,048 bytes, exact field set, release cache epoch, topic
|
||||
allowlist, TTL, self/duplicate/stale sequence를 검증하고 query key/data를
|
||||
허용하지 않는다.
|
||||
- `storage-keys.ts`와 browser storage adapter가 key별 closed value codec,
|
||||
schema/TTL, 기본 16,384-byte cap과 memory fallback envelope를 적용한다.
|
||||
- `cross-tab-invalidation.test.ts`, `tanstack-cache-coordinator.test.ts`,
|
||||
`storage-registry.test.ts`, runtime adapter/component test가 deterministic
|
||||
동작과 production wiring을 검증한다.
|
||||
- 상세 상태와 남은 session/account epoch, native multi-tab evidence 및 optional
|
||||
IndexedDB query persistence는
|
||||
[Client cache and browser storage platform](./client-cache-and-storage.md)에
|
||||
기록한다.
|
||||
|
||||
**Rollback**
|
||||
|
||||
RP-03은 application port를 유지하면서 이전 HTTP compatibility adapter 또는
|
||||
@@ -826,11 +849,11 @@ owner와 만료 시한이 있는 quarantine만 허용한다.
|
||||
사용한다.
|
||||
- 기본 Playwright는 `build` + `preview`의 실제 `dist`를 Chromium, Firefox,
|
||||
WebKit에서 검사하고 별도 compact project를 제공한다. 개발 피드백용 Vite
|
||||
profile은 `playwright.dev.config.js`로 분리했다.
|
||||
profile은 `playwright.dev.config.ts`로 분리했다.
|
||||
- Storybook public primitive story, interaction과 axe test, wide/compact/
|
||||
pseudo/dark/state surface의 pinned Chromium visual baseline 4종을 CI evidence로
|
||||
연결했다.
|
||||
- V8 coverage와 9개 high-risk module을 대상으로 40개 scoped threshold를
|
||||
- V8 coverage와 12개 high-risk module을 대상으로 52개 scoped threshold를
|
||||
적용하고 threshold 미달 fixture를 차단한다.
|
||||
- deterministic clock/random/scheduler/storage, unexpected console/page error/
|
||||
request failure 정책, 무소유 skip과 full-screen mask 금지 gate를 제공한다.
|
||||
@@ -959,10 +982,10 @@ gate 없이는 production promotion을 통과했다고 보지 않는다.
|
||||
|
||||
| recipe | 기본 경계 | 반드시 다룰 실패 |
|
||||
| --- | --- | --- |
|
||||
| realtime | subscribe/unsubscribe, reconnect, resume, heartbeat | disconnect, duplicate/out-of-order, auth expiry |
|
||||
| offline/IndexedDB | versioned repository와 migration | quota, corruption, migration rollback |
|
||||
| Service Worker/PWA | cache ownership와 update controller | stale worker, update loop, offline fallback |
|
||||
| file transfer | upload/download, progress, cancel | size/type rejection, abort, expired URL |
|
||||
| realtime | SSE/WebSocket connection + inbound event, Web Push window/worker, bounded Poll lease | disconnect, duplicate/out-of-order/gap, cursor/auth expiry, queue overflow, permission/subscription failure |
|
||||
| offline/IndexedDB/OPFS | feature repository, resumable migration, journal-backed large-object store | blocked/versionchange, quota, corruption, revision/generation conflict, partial object write |
|
||||
| Service Worker/Cache Storage | public response cache ownership와 update controller | stale worker, incomplete candidate, integrity/policy rejection, offline fallback |
|
||||
| File/Blob/picker/download | opaque file vault, upload session, browser handoff/streamed save | dismissal/permission, size/type/signature, abort/session expiry, integrity/partial save |
|
||||
| generated API | generated client를 gateway 뒤에 감싸는 facade | contract drift, unsupported field |
|
||||
| feature flag | typed key, default와 stale policy | provider unavailable, unknown flag |
|
||||
| Web Worker | request/result/cancel protocol | crash, stale result, transfer failure |
|
||||
@@ -1027,6 +1050,137 @@ revert하고 RP-11을 유지한다. 여러 vendor를 되돌릴 수 없는 한 co
|
||||
sentinel이 built `dist`에 없는지 검사한다.
|
||||
- 상세 도입/배치/검증/제거 절차는
|
||||
`docs/architecture/optional-adapter-recipes.md`에 기록했다.
|
||||
- 2026-07-27 후속 설계에서 VD-11을 추가하고 File/Blob/picker/download를
|
||||
picker/content/upload-session/download-delivery로 분리했다. IndexedDB는
|
||||
schema/codec, transaction complete, blocked/versionchange, resumable migration
|
||||
계약으로 보강하고, OPFS는 IndexedDB journal 기반 crash-safe large-object
|
||||
sub-capability, Cache Storage는 public response 전용 release-candidate
|
||||
sub-capability로 명시했다.
|
||||
- 같은 후속 구현에서 해당 browser-native sub-capability를 정책 주입형
|
||||
`AVAILABLE_NOT_COMPOSED` reference runtime으로 `src/adapters`에 구현했다.
|
||||
제품별 schema/codec/query/retention/cache allowlist는 주입하며, 선택 전에는
|
||||
bootstrap import를 정적 gate로 금지하고 실제 browser conformance suite로
|
||||
native API 경로를 검증한다.
|
||||
- `browser-file-storage-contracts.ts`와 deterministic fake/test가 CAS/idempotency,
|
||||
quota, picker dismissal, bounded range/stream, partial OPFS write visibility,
|
||||
generation conflict, cache exact query/integrity policy를 실행 가능하게 검증한다.
|
||||
reference runtime source는 존재하지만 production composition의 optional
|
||||
runtime module 수는 계속 0개이며, 실제 선택 branch는
|
||||
real-browser/fault/runbook evidence 없이 `INSTALLED`로 승격할 수 없다.
|
||||
- 2026-07-28 후속 설계와 구현에서 VD-12를 추가했다. presigned capability는
|
||||
BFF control plane과 browser/object-storage data plane을 분리하고, multipart
|
||||
upload는 server-authoritative reconcile과 non-secret IndexedDB checkpoint,
|
||||
streaming download는 bounded closed-result source, Image CDN은 opaque asset과
|
||||
named preset 기반 responsive descriptor로 구현한다. 제품 endpoint/CDN policy가
|
||||
없으므로 모두 `AVAILABLE_NOT_COMPOSED`이며 bootstrap에는 연결하지 않는다.
|
||||
- generic multi-tab recipe와 별도로, installed TanStack query의 좁은
|
||||
invalidate-only protocol은 2026-07-28 RP-03 infrastructure로 선택·조립했다.
|
||||
query state/payload를 복제하지 않고 persistence는 계속 disabled다. 기존
|
||||
IndexedDB reference runtime은 이 선택 때문에 bootstrap에 조립되지 않는다.
|
||||
- 같은 날 VD-28과
|
||||
[Realtime events, Web Push, and bounded polling](./realtime-events-web-push-and-bounded-polling.md)을
|
||||
추가했다. SSE, WebSocket, Web Push와 Polling을 별도 delivery capability로
|
||||
분리하고 공통 delivery guarantee 없음, duplicate-tolerant CURSOR 처리,
|
||||
gap/snapshot checkpoint, scope generation, bounded lifecycle과 promotion 계약을
|
||||
닫았다. 후속 RT-01~04에서 공통 authority, fetch-stream SSE, bounded Polling,
|
||||
closed WebSocket, Web Push window/worker와 단일 writer handoff/reconnect
|
||||
coordinator를 구현해 reusable runtime은 `AVAILABLE_NOT_COMPOSED`로 올렸다.
|
||||
제품 endpoint/event registry/provider selection과 production composition은
|
||||
없으므로 product는 계속 `NOT_SELECTED`다.
|
||||
- 2026-07-28 API-05/API-06 공통 선행 구현으로 Browser RPC V3 operation/provider
|
||||
registry, typed application port, provider-neutral unary/server-stream lifecycle
|
||||
coordinator와 fail-closed unavailable adapter를 추가했다. exact registry join,
|
||||
bounded retry/deadline/abort, stream idle/total/message/terminal, scope generation을
|
||||
test하므로 이 공통 층만 `AVAILABLE_NOT_COMPOSED`다. descriptor/codegen,
|
||||
Connect-Web/official grpc-web transport와 실제 proxy/browser conformance는
|
||||
포함하지 않는다.
|
||||
|
||||
### 12.1 Browser data 후속 work package
|
||||
|
||||
RP-12의 reference runtime 존재는 browser data platform 전체의 구현 완료를
|
||||
뜻하지 않는다. 현재 상태와 상세 exit criteria는
|
||||
[Browser data capability completion ledger](./browser-data-capability-completion-ledger.md)를
|
||||
단일 기준으로 사용한다. 후속 구현은 다음 독립 package로 진행한다.
|
||||
|
||||
| 순서 | work package | 핵심 exit |
|
||||
| --- | --- | --- |
|
||||
| BD-01 | scope-safe client cache | logout/account switch의 cancel/fence/clear/dispose와 late-result 차단 |
|
||||
| BD-02 | Range resumable download | precondition-mode별 allowed `200/206/412/416`, seek/truncate와 final integrity |
|
||||
| BD-03 | origin storage lifecycle | cross-store pressure/GC, bounded maintenance와 N-1 migration/rollback |
|
||||
| BD-04 | transfer operational composition | atomic config/factory, account teardown, pause/retention과 provider harness |
|
||||
| BD-05 | Image descriptor delivery | fixed BFF provider, refresh fence, safe picture projection과 actual CDN 증거 |
|
||||
|
||||
각 package는 다음 순서를 지킨다.
|
||||
|
||||
```text
|
||||
ledger + ADR accepted
|
||||
-> closed port/policy/schema
|
||||
-> deterministic contract/fault evidence
|
||||
-> reference runtime + boundary/removal gate
|
||||
-> AVAILABLE_NOT_COMPOSED
|
||||
-> product/provider selection
|
||||
-> bootstrap composition + three-engine evidence
|
||||
-> COMPOSED
|
||||
```
|
||||
|
||||
directory/persistent file handle, Query persistence, offline mutation,
|
||||
Service Worker fetch interception, private/range Cache와 app-managed background
|
||||
transfer는 위 package에 암묵적으로 포함하지 않는다. 각각 `NOT_SELECTED`에서
|
||||
별도 ADR과 owner 승인을 거쳐야 한다.
|
||||
|
||||
### 12.2 API·Schema·Mapper·Server State 후속 work package
|
||||
|
||||
REST reference vertical의 `COMPOSED` 판정과 multi-protocol production hardening을
|
||||
구분한다. 현재/목표 상태, protocol 선택 기준과 완료 조건은
|
||||
[API contract, Schema, Mapper와 Server State](./api-contract-schema-mapper-and-server-state.md)가
|
||||
소유한다. Browser Protobuf runtime과 gateway branch의 세부 기준은
|
||||
[Protobuf browser transport와 REST Gateway](./protobuf-browser-transport-and-rest-gateway.md)를
|
||||
따른다.
|
||||
|
||||
| 순서 | work package | 핵심 exit |
|
||||
| --- | --- | --- |
|
||||
| API-01 | REST v2 execution | auth fail-close/final invariant, collision-free registry, total deadline, bounded status/media decoder |
|
||||
| API-02 | typed Schema·Mapper | codec/mapper type proof, semantic fingerprint/provenance, scalar/null/enum/collection ceiling |
|
||||
| API-03 | strict Server State | bound query definition/key/policy, cursor page, mutation concurrency와 revision/CAS optimistic rollback |
|
||||
| API-04 | persisted GraphQL | allowlisted manifest, data/errors/partial state, cost/auth/CSRF와 actual router conformance |
|
||||
| API-05 | gRPC-Web | descriptor/codegen, frame/trailer/status, unary/server-stream budget와 actual proxy/browser conformance |
|
||||
| API-06 | Connect-Web | exact Connect/gRPC-Web transport row, JSON/binary/GET/EndStream, bounded decode와 actual server/browser conformance |
|
||||
| API-07 | Protobuf REST Gateway | schema/codegen provenance, HttpRule/ProtoJSON/OpenAPI와 selected gateway conformance |
|
||||
| API-08 | operations/composition | contract-set release binding, provider kill switch, coherent rollback과 protocol dependency removal |
|
||||
|
||||
API-05/API-06가 공유하는 provider-neutral registry와 lifecycle coordinator는
|
||||
`AVAILABLE_NOT_COMPOSED`다. 그러나 각 package의 wire adapter exit는 아직
|
||||
충족되지 않았으므로 API-05/API-06 전체를 완료로 표시하지 않는다.
|
||||
|
||||
GraphQL, Connect, gRPC-Web과 REST Gateway는 제품 operation/provider 선택 전
|
||||
`NOT_SELECTED`다. 설계 문서가 있다는 이유로 dependency/generated source/proxy
|
||||
config를 기본 bundle에 넣지 않는다. browser client/bidirectional streaming은
|
||||
구현 backlog가 아니라 `PLATFORM_LIMITED`이며 별도 transport 요구로 다시 설계한다.
|
||||
|
||||
권장 순서는 API-01 → API-02 → API-03이다. API-04~07은 실제 backend contract가
|
||||
선택된 branch만 구현한다.
|
||||
|
||||
### 12.3 Realtime delivery 후속 work package
|
||||
|
||||
copyable recipe의 교육용 `RealtimePort`와 fake는 production runtime 증거가
|
||||
아니다. 현재 상태,
|
||||
transport 선택, event authority와 완료 기준은
|
||||
[Realtime events, Web Push, and bounded polling](./realtime-events-web-push-and-bounded-polling.md)이
|
||||
소유한다.
|
||||
|
||||
| 순서 | work package | 핵심 exit |
|
||||
| --- | --- | --- |
|
||||
| RT-01 | common event authority | closed registry/envelope, scope generation, sequential apply, dedupe/gap/cursor와 snapshot reset |
|
||||
| RT-02 | SSE + bounded polling | bounded fetch-stream parser, reconnect/heartbeat와 visible-only single-flight Poll lease |
|
||||
| RT-03 | WebSocket | fixed subprotocol, heartbeat/close, bounded inbound/outbound queue와 resume |
|
||||
| RT-04 | Web Push | permission/subscription facade, worker push/click, safe notification와 provider lifecycle |
|
||||
| RT-05 | product composition/operations | backend replay/outbox/auth, hosting/provider/browser evidence, canary/kill switch/drill |
|
||||
|
||||
RT-01~04의 source, contract/native test, boundary/bundle/removal gate가 통과한
|
||||
runtime만 `AVAILABLE_NOT_COMPOSED`로 올린다. 실제 event/notification registry와
|
||||
provider가 bootstrap/worker에 조립된 선택 capability만 `COMPOSED`다.
|
||||
exactly-once/global ordering, always-on background connection과 timely
|
||||
cross-browser Web Push guarantee는 구현 backlog가 아니라 `PLATFORM_LIMITED`다.
|
||||
|
||||
## 11. Vendor decision gate
|
||||
|
||||
@@ -1042,6 +1196,14 @@ revert하고 RP-11을 유지한다. 여러 vendor를 되돌릴 수 없는 한 co
|
||||
| VD-08 | 10 전 | Storybook/visual 방식 | dev-only Storybook + local Playwright baseline | cloud review/별도 배포 |
|
||||
| VD-09 | 11 전 | vulnerability/license/SBOM/provenance | 임의 PASS 금지, `FAIL_UNVERIFIED` | release promotion |
|
||||
| VD-10 | 12 전 | optional capability 요구 | 설치하지 않음 | 해당 recipe만 |
|
||||
| VD-23 | API-01 전 | protocol 선택과 REST execution | operation별 protocol 고정, installed REST 유지 | REST v2 hardening |
|
||||
| VD-24 | API-02 전 | runtime Schema와 Mapper proof | typed codec → pure Result mapper | multi-protocol contract/codegen |
|
||||
| VD-25 | API-03 전 | Server State Cache lifecycle | TanStack sole owner, bound query/mutation profile | query/cache/mutation hardening |
|
||||
| VD-26 | API-04 전 | GraphQL | persisted operation only, 기본 미선택 | GraphQL operation family |
|
||||
| VD-27 | API-05 전 | gRPC-Web | unary/server-stream만, 기본 미선택 | gRPC-Web operation family |
|
||||
| VD-29 | API-06 전 | Connect-Web/Connect | exact runtime/wire profile, 기본 미선택 | Connect operation family |
|
||||
| VD-30 | API-07 전 | Protobuf/REST Gateway | current REST는 BFF 유지, direct gateway 기본 미선택 | schema/codegen/gateway branch |
|
||||
| VD-28 | RT-01 전 | realtime/Web Push/Polling | focus refetch 우선, transport별 opt-in과 bounded fallback | realtime delivery capability |
|
||||
|
||||
각 decision에는 실제 요구, bundle/runtime/a11y/security 영향, local facade 방식,
|
||||
대안, fallback/migration/removal, owner와 재검토 시점을 기록한다.
|
||||
@@ -1054,7 +1216,7 @@ revert하고 RP-11을 유지한다. 여러 vendor를 되돌릴 수 없는 한 co
|
||||
|
||||
| checkpoint | 포함 범위 | 다음 단계 진입 조건 | 대표 rollback 사유 |
|
||||
| --- | --- | --- | --- |
|
||||
| RP-01 | tooling | TS/JS fixture와 gate 통과 | TS 파일이 검사에서 누락 |
|
||||
| RP-01 | tooling | TS/TSX fixture와 gate 통과 | TS 파일이 검사에서 누락 |
|
||||
| RP-02 | application boundary | composition integration 통과 | UI가 raw adapter에 의존 |
|
||||
| RP-03 | HTTP/query | request/state negative matrix 통과 | query 손실, retry/submit 폭주 |
|
||||
| RP-04 | routing/recovery | registry와 reload-loop drill 통과 | invalid URL API 호출, reload loop |
|
||||
|
||||
@@ -116,7 +116,7 @@ bootstrap은 page별 orchestration이나 업무 규칙을 소유하지 않는다
|
||||
| `src/adapters` | HTTP, auth, storage, cache, telemetry 구현 | outbound adapter |
|
||||
| `src/bootstrap` | runtime config와 구현 조립 | 유일한 composition root |
|
||||
| `src/contracts` | 여러 계층의 registry가 혼재 | 소유 계층으로 분산 |
|
||||
| `src/features/reference-feature` | 완전한 제거 가능 수직 예제 | installed contribution과 8단계 제거 gate 유지 |
|
||||
| `src/features/reference-feature` | 완전한 제거 가능 수직 예제 | installed contribution과 제거 gate 유지 |
|
||||
|
||||
현재 구조가 잘 제공하는 기반은 다음과 같다.
|
||||
|
||||
@@ -132,10 +132,11 @@ bootstrap은 page별 orchestration이나 업무 규칙을 소유하지 않는다
|
||||
|
||||
RP-02 구현으로 다음 경계는 실행 경로에 연결됐다.
|
||||
|
||||
- `src/bootstrap/composition-root.js`가 만든 application을 production
|
||||
- `src/bootstrap/composition-root.ts`가 만든 application을 production
|
||||
`ApplicationProvider`가 실제 React tree에 주입한다.
|
||||
- `createApplication`은 session, preference, diagnostics, runtime query의
|
||||
input API만 반환하며 storage, telemetry, release output port를 숨긴다.
|
||||
- `createApplication`은 session, preference, diagnostics, runtime query와 typed
|
||||
feature input registry만 반환하며 storage, telemetry, release output port를
|
||||
숨긴다.
|
||||
- bootstrap composition 결과는 raw output port를 반환하지 않고
|
||||
application과 React infrastructure만 반환한다.
|
||||
- presentation의 direct fetch/browser storage/concrete adapter/TanStack import와
|
||||
@@ -153,6 +154,16 @@ RP-03 구현으로 HTTP와 server-state 경계도 다음처럼 연결됐다.
|
||||
정리를 테스트한다.
|
||||
- HTTP가 자동 network retry를 소유하고 query/mutation adapter의 vendor retry는
|
||||
비활성화한다.
|
||||
- reference HTTP operation map은 operation ID마다 route ID, request shape와
|
||||
성공 model type을 결합한다. runtime schema와 mapper를 통과한 raw executor는
|
||||
단일 binder에서만 typed executor로 승격되므로 gateway별 응답 cast가 없다.
|
||||
|
||||
위 항목은 reference REST vertical의 `COMPOSED` 증거다. auth owner final-request
|
||||
invariant, auth unavailable fail-close, total deadline, bounded response decoder,
|
||||
typed Schema/Mapper proof, complete pagination과 mutation concurrency 같은
|
||||
production hardening delta는
|
||||
[API contract, Schema, Mapper와 Server State](./api-contract-schema-mapper-and-server-state.md)에
|
||||
별도 `DESIGNED_NOT_IMPLEMENTED`로 기록한다.
|
||||
|
||||
RP-04에서 route 실행 불일치는 닫혔다. route registry와 runtime map은
|
||||
Data Router tree, codec, surface, title, navigation, chunk/release recovery의
|
||||
@@ -163,6 +174,12 @@ contract/presentation은 `src/features/reference-feature`가 소유하고, gener
|
||||
installed catalog만 bootstrap과 router에 노출된다. 제거 gate는 feature와 test를
|
||||
삭제한 복제본에서 전체 P0 경로를 다시 실행한다.
|
||||
|
||||
generic application의 `ApplicationFeatureInputs`는 concrete feature를 import하지
|
||||
않는 open interface다. 각 feature application API가 module augmentation으로
|
||||
자신의 literal ID와 input shape를 기여하고, `features.get(id)`는 ID별 정확한
|
||||
input type을 반환한다. 잘못된 ID/input과 설치 누락은 각각 compile-time negative
|
||||
fixture와 runtime guard로 닫는다.
|
||||
|
||||
RP-06에서 inbound form/page 경계도 실행됐다. `src/presentation/forms`는 Zod
|
||||
presentation schema, controlled field state, error focus, 422 allowlist,
|
||||
pending/deduplication과 dirty navigation을 local facade로 감싼다.
|
||||
@@ -198,7 +215,7 @@ adapter만 교체한다.
|
||||
| Routing과 navigation | inbound | route input/controller | React Router | 필수 |
|
||||
| Form submit과 validation 표시 | inbound | command input port 소비 | React form controller | 필수 |
|
||||
| Server-state query hook | inbound bridge | application input API 소비 | TanStack Query hook | 필수 |
|
||||
| 외부 push event 처리 | inbound | event input port | WebSocket/SSE listener | 선택 |
|
||||
| validated external server event | inbound | event input port | SSE/WebSocket listener, Service Worker push handler | 선택 |
|
||||
| 도메인 API 접근 | outbound | application | Fetch gateway | 필수 |
|
||||
| 인증 session | outbound | application | 외부 auth owner/SDK | 필수 seam |
|
||||
| Credential attachment | outbound | transport 또는 auth integration | auth request decorator | 필수 seam |
|
||||
@@ -396,15 +413,19 @@ Inbound adapter가 해당 type을 application command와 query로 변환한다.
|
||||
### 8.3 예측 가능한 실패를 typed result로 반환한다
|
||||
|
||||
검증 실패, 인증 필요, conflict, network failure처럼 사용자 흐름에
|
||||
포함되는 실패는 `Result<Value, Failure>`로 반환한다. programmer error와
|
||||
포함되는 실패는 `Result<Value, Failure = AppFailure>`로 반환한다. programmer error와
|
||||
불변식 위반을 모두 일반 API 실패로 숨기지는 않는다.
|
||||
|
||||
```ts
|
||||
export type Result<Value, Failure> =
|
||||
export type Result<Value, Failure = AppFailure> =
|
||||
| Readonly<{ ok: true; value: Value }>
|
||||
| Readonly<{ ok: false; error: Failure }>;
|
||||
```
|
||||
|
||||
`AppFailure.kind`는 error registry의 key에서 파생한다. adapter가 받은 외부
|
||||
오류 code는 이 닫힌 vocabulary로 매핑한 뒤 application 경계를 통과하며,
|
||||
transport 문맥의 `ApiFailure`는 같은 type을 가리키는 호환 alias다.
|
||||
|
||||
### 8.4 Input port를 기술별로 합치지 않는다
|
||||
|
||||
`ApplicationService` 한 개에 모든 메서드를 계속 추가하지 않는다.
|
||||
@@ -885,9 +906,11 @@ capability의 기본 정책, port 또는 안전한 no-op 구현과 composition
|
||||
|
||||
| Adapter | 도입 조건 | 기본 상태 |
|
||||
| --- | --- | --- |
|
||||
| WebSocket/SSE | 실시간 server event 필요 | opt-in recipe 제공, 미설치 |
|
||||
| IndexedDB | 큰 offline data 또는 durable queue 필요 | opt-in recipe 제공, 미설치 |
|
||||
| Service Worker/PWA | offline shell과 installability 필요 | opt-in recipe 제공, 미설치 |
|
||||
| SSE/WebSocket/bounded polling | active document의 server event 또는 duplex protocol 필요 | common target `DESIGNED_NOT_IMPLEMENTED`, product `NOT_SELECTED` |
|
||||
| Web Push | inactive browser의 user-visible notification 필요 | target `DESIGNED_NOT_IMPLEMENTED`, product `NOT_SELECTED` |
|
||||
| IndexedDB/OPFS | 큰 offline data, durable queue 또는 large local object 필요 | native reference runtime 제공, 미조립 |
|
||||
| Cache Storage | 승인된 public HTTP representation offline cache 필요 | native reference runtime 제공, 미조립 |
|
||||
| Service Worker/PWA | offline shell과 installability 필요 | lifecycle recipe 제공, 미설치 |
|
||||
| Offline mutation queue | 재연결 후 명령 재처리 필요 | 미설치 recipe |
|
||||
| Feature flag | remote rollout/kill switch 필요 | opt-in recipe 제공, 미설치 |
|
||||
| Translation catalog vendor | 원격 catalog·복수 namespace 운영 필요 | 기본 locale facade 뒤에 미설치 |
|
||||
@@ -896,7 +919,8 @@ capability의 기본 정책, port 또는 안전한 no-op 구현과 composition
|
||||
| OpenTelemetry | 조직 trace 연계 필요 | opt-in recipe 제공, 미설치 |
|
||||
| Web Worker | CPU 작업이 main thread를 막음 | opt-in recipe 제공, 미설치 |
|
||||
| Notification | 사용자 권한 기반 browser notification 필요 | opt-in recipe 제공, 미설치 |
|
||||
| Clipboard/File/Media | 해당 browser capability 필요 | opt-in recipe 제공, 미설치 |
|
||||
| File/Blob/picker/download | 해당 file workflow 필요 | native reference runtime 제공, 미조립 |
|
||||
| Clipboard/Media | 해당 browser capability 필요 | opt-in recipe 제공, 미설치 |
|
||||
| Image CDN adapter | responsive image transform 필요 | 미설치 recipe |
|
||||
| Virtualization | 대량 list rendering이 측정상 병목 | opt-in recipe 제공, 미설치 |
|
||||
| OpenAPI generator | backend 계약에서 client 생성 필요 | opt-in recipe 제공, 미설치 |
|
||||
@@ -906,11 +930,24 @@ capability의 기본 정책, port 또는 안전한 no-op 구현과 composition
|
||||
|
||||
선택 adapter는 “나중에 쓸 수 있으므로” 기본 bundle에 넣지 않는다. 도입
|
||||
조건, 보안 영향, bundle 비용과 제거 방법이 확인된 경우에만 추가한다.
|
||||
현재 구현된 공통 catalog, TypeScript contract/fake와 blocking gate는
|
||||
SSE, WebSocket, Web Push와 bounded polling의 서로 다른 delivery 의미,
|
||||
inbound/outbound 분리, resume·gap·lifecycle과 현재 상태는
|
||||
[Realtime events, Web Push, and bounded polling](./realtime-events-web-push-and-bounded-polling.md)을
|
||||
따른다.
|
||||
Web Push는 subscription/provider/worker delivery를, 별도 Notification 행은
|
||||
permission과 user-visible rendering facade를 뜻한다. bounded polling은 inbound
|
||||
push adapter가 아니라 Query bridge 또는 application orchestrator가 기존 HTTP
|
||||
operation을 schedule하는 policy다.
|
||||
현재 구현된 공통 catalog, TypeScript contract/fake, browser-native reference
|
||||
runtime과 blocking gate는
|
||||
`docs/architecture/optional-adapter-recipes.md`와
|
||||
`config/recipes/frontend-capability-recipes.json`을 따른다. 이 recipe source를
|
||||
production에서 직접 import하는 것은 금지하며 선택한 contract만 application
|
||||
소유 경계로 이동한다.
|
||||
소유 경계로 이동한다. 실제 API를 호출하는 `referenceRuntime`은 catalog에
|
||||
`sourceRoots`가 등록된 browser file/IndexedDB/OPFS/Cache/transfer 계열에만
|
||||
존재한다. realtime target은 아직 `DESIGNED_NOT_IMPLEMENTED`다. 구현된
|
||||
reference runtime도 dataset/schema/codec/query/policy와 제품 owner가 없으면
|
||||
bootstrap에 연결하지 않는다.
|
||||
|
||||
## 19. 새 outbound adapter 추가 recipe
|
||||
|
||||
@@ -1213,8 +1250,8 @@ design-system primitive를 조합하고 controller hook을 통해 application을
|
||||
| Reference feature | full vertical integration과 complete removal |
|
||||
|
||||
TypeScript 전환 후에는 source뿐 아니라 test와 architecture/security
|
||||
fixture도 typecheck 또는 lint 대상이어야 한다. `.ts/.tsx`가 JS 전용 glob을
|
||||
우회하지 않도록 한다.
|
||||
fixture도 typecheck 또는 lint 대상이어야 한다. 모든 실행 fixture를
|
||||
`.ts/.tsx`로 유지해 확장자별 검사 우회를 허용하지 않는다.
|
||||
|
||||
Coverage는 단순 report 생성이 아니라 branch/function/line threshold를
|
||||
blocking gate로 둔다. 수치만 올리기 위한 구현 세부 테스트보다 port
|
||||
@@ -1230,13 +1267,13 @@ contract와 실패 분기를 우선한다.
|
||||
- [ ] `main`은 raw ports 대신 application API를 제공한다.
|
||||
- [ ] `contracts`는 unrestricted 우회 계층이 아니다.
|
||||
- [ ] dependency rule과 문서의 source of truth가 하나다.
|
||||
- [ ] TypeScript와 TSX도 동일한 architecture/security lint를 받는다.
|
||||
- [x] TypeScript와 TSX도 동일한 architecture/security lint를 받는다.
|
||||
|
||||
### 26.2 Runtime composition
|
||||
|
||||
- [ ] runtime timeout/retry 설정이 실제 HTTP transport에 반영된다.
|
||||
- [ ] QueryClient와 feature query bridge가 실제 route에서 동작한다.
|
||||
- [ ] session UI API와 credential attachment가 분리되어 있다.
|
||||
- [x] runtime timeout/retry 설정이 실제 HTTP transport에 반영된다.
|
||||
- [x] QueryClient와 feature query bridge가 실제 route에서 동작한다.
|
||||
- [x] session UI API와 credential attachment가 분리되어 있다.
|
||||
- [x] diagnostics와 telemetry가 HTTP/render/storage/cache failure 경로에
|
||||
연결된다.
|
||||
- [x] `pagehide`에서 bounded telemetry queue를 flush하고 adapter `dispose`가
|
||||
@@ -1245,8 +1282,9 @@ contract와 실패 분기를 우선한다.
|
||||
### 26.3 HTTP와 validation
|
||||
|
||||
- [ ] path, search, body를 각각 검증하고 직렬화한다.
|
||||
- [ ] schema가 반환한 normalized data를 실제 request에 사용한다.
|
||||
- [ ] timeout과 AbortSignal listener가 모든 반환 경로에서 정리된다.
|
||||
- [x] schema가 반환한 normalized data를 실제 request에 사용한다.
|
||||
- [x] 생성된 timeout과 AbortSignal listener가 현재 HTTP attempt의 terminal
|
||||
경로에서 정리된다.
|
||||
- [ ] retry는 runtime cap, idempotency와 `Retry-After`를 따른다.
|
||||
- [ ] raw payload와 credential이 failure나 log에 포함되지 않는다.
|
||||
|
||||
@@ -1256,7 +1294,7 @@ contract와 실패 분기를 우선한다.
|
||||
- [x] params/search schema가 실제 navigation에서 실행된다.
|
||||
- [x] loading/error/chunk/access metadata가 실행 behavior와 연결된다.
|
||||
- [ ] local, URL, server, session, persisted state가 분류 규칙을 따른다.
|
||||
- [ ] server state를 별도 global store에 중복 보관하지 않는다.
|
||||
- [x] reference server state를 별도 global store에 중복 보관하지 않는다.
|
||||
|
||||
### 26.5 Reference feature
|
||||
|
||||
@@ -1268,7 +1306,7 @@ contract와 실패 분기를 우선한다.
|
||||
|
||||
### 26.6 품질
|
||||
|
||||
- [ ] source와 test가 strict TypeScript 검사를 받는다.
|
||||
- [x] source와 test가 strict TypeScript 검사를 받는다.
|
||||
- [ ] React Hooks와 JSX accessibility lint가 blocking이다.
|
||||
- [ ] coverage threshold가 blocking이다.
|
||||
- [ ] MSW integration, component, 3-engine E2E와 axe가 통과한다.
|
||||
@@ -1298,10 +1336,12 @@ contract와 실패 분기를 우선한다.
|
||||
|
||||
### P2: 프로젝트별 선택 capability
|
||||
|
||||
WebSocket/SSE, offline/IndexedDB, Service Worker, feature flag, product
|
||||
SSE/WebSocket, Web Push, bounded polling, offline/IndexedDB, Service Worker, feature flag, product
|
||||
analytics, vendor error reporting, worker, virtualization, OpenAPI generation,
|
||||
global store와 cloud visual-review service는 실제 프로젝트 요구와 측정 결과에
|
||||
따라 추가한다.
|
||||
GraphQL, Connect/gRPC-Web, Protobuf REST Gateway, global store와 cloud
|
||||
visual-review service는 실제 프로젝트 요구와 측정 결과에 따라 추가한다.
|
||||
GraphQL과 browser Protobuf/Gateway의 선택·설치 조건은 VD-26과
|
||||
VD-27/VD-29/VD-30을 따른다.
|
||||
|
||||
P2 adapter를 많이 설치하는 것은 skeleton 완성도의 기준이 아니다.
|
||||
안전한 경계, 도입 recipe, 테스트 계약과 제거 가능성이 준비되어 있는지가
|
||||
|
||||
+29
-12
@@ -10,6 +10,7 @@ adapters implement application-owned ports and are assembled only in
|
||||
| `application` | use cases, ports, orchestration, view-models | domain and application siblings |
|
||||
| `presentation` | routes, components, user interaction and view state | application public API and shared UI |
|
||||
| `adapters` | browser and third-party implementations of application ports | application ports and limited domain values |
|
||||
| `features/<id>` | removable vertical domain/application/contracts/adapters/presentation slice | the same inward rule plus platform public boundaries |
|
||||
| `bootstrap` | runtime configuration, adapter construction and React mount | all selected runtime modules |
|
||||
|
||||
The following edges are forbidden:
|
||||
@@ -18,25 +19,41 @@ The following edges are forbidden:
|
||||
- application to presentation, concrete adapters, bootstrap, React, or browser globals
|
||||
- presentation to concrete adapters, raw DTO schemas, or storage implementations
|
||||
- an adapter to presentation, bootstrap internals, or another concrete adapter
|
||||
- feature domain/application to its presentation or outbound adapter, and
|
||||
feature presentation to its outbound adapter
|
||||
|
||||
`bootstrap` contains composition only. Business rules and page-specific
|
||||
orchestration belong to domain/application.
|
||||
|
||||
This table is the current coarse-grained rule. The
|
||||
[ports, adapters, and feature-boundary target](./frontend-ports-adapters-and-boundaries.md)
|
||||
defines the missing application input boundary, explains that `presentation`
|
||||
acts as the inbound adapter, and separates current outbound adapters from
|
||||
project-selected capabilities. The
|
||||
[platform capability review](./frontend-platform-capability-review.md) records
|
||||
where the current composition still bypasses this intended rule.
|
||||
The [ports, adapters, and feature-boundary contract](./frontend-ports-adapters-and-boundaries.md)
|
||||
explains how `presentation` acts as the inbound adapter and how feature
|
||||
application APIs augment the generic typed input registry. Concrete output
|
||||
ports are composed in bootstrap and stay hidden behind the application facade.
|
||||
Project-selected capabilities still implement the same output boundaries.
|
||||
|
||||
Architecture reports use this shape:
|
||||
`check:architecture` keeps dependency-cruiser's report and adds the
|
||||
authoritative TypeScript-aware graph below it:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"generatedAt": "ISO-8601",
|
||||
"rules": [{ "name": "rule-id", "severity": "error", "violations": 0 }],
|
||||
"summary": { "errors": 0, "warnings": 0 }
|
||||
"staticImportGraph": {
|
||||
"analyzer": "babel-parser-node-resolver",
|
||||
"modules": [],
|
||||
"dependencies": [],
|
||||
"unresolved": [],
|
||||
"parseFailures": [],
|
||||
"cycles": [],
|
||||
"violations": [],
|
||||
"summary": { "errors": 0 },
|
||||
"fixtureChecks": { "passed": true, "checks": [], "failures": [] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The graph scans TypeScript and TSX, including static, dynamic, type, CommonJS
|
||||
and JSDoc import references. It applies the path rules from
|
||||
`.dependency-cruiser.json`, requires explicit TypeScript extensions for local
|
||||
source imports, and fails closed on JavaScript-family source/specifiers,
|
||||
unsupported rule shapes, unresolved imports, parse failures, error-severity
|
||||
layer violations, or cycles. Regression fixtures prove the allowed resolver
|
||||
path and each rejection class.
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
|
||||
이 문서는 도메인과 무관한 선택형 frontend capability를 실제 프로젝트에
|
||||
도입하는 실행 가이드다. 기본 스켈레톤에는 vendor runtime을 설치하지 않는다.
|
||||
`RECIPE_AVAILABLE`은 계약·fake·failure policy가 준비됐다는 뜻이며 실제 provider,
|
||||
runtime behavior 또는 production readiness를 뜻하지 않는다.
|
||||
`RECIPE_AVAILABLE`은 catalog에 복사해 좁힐 recipe가 있다는 availability
|
||||
표시다. runtime 구현이나 제품 선택·조립 상태가 아니다. 현재 catalog의 product
|
||||
selection과 production composition은 별도로 `NOT_SELECTED`/미설치다.
|
||||
일부 browser-native capability에는 dependency 없는
|
||||
`referenceRuntime.status=AVAILABLE_NOT_COMPOSED` 구현이 함께 있지만, 이것도
|
||||
제품 dataset·owner·policy가 정해져 bootstrap에 연결되기 전에는 설치된 기능이나
|
||||
production readiness를 뜻하지 않는다.
|
||||
|
||||
## 1. 현재 상태와 파일 지도
|
||||
|
||||
@@ -14,13 +19,57 @@ runtime behavior 또는 production readiness를 뜻하지 않는다.
|
||||
| TypeScript port | `recipes/frontend-capabilities/contracts.ts` | 아니오 |
|
||||
| fake/unavailable | `recipes/frontend-capabilities/fake-adapters.ts` | 아니오 |
|
||||
| contract test | `tests/recipes/optional-capability-contracts.test.ts` | 아니오 |
|
||||
| 정적/번들 gate | `scripts/check-optional-recipes.mjs` | build 도구 |
|
||||
| negative fixture | `scripts/check-optional-recipe-fixtures.mjs` | 아니오 |
|
||||
| 완전 제거 gate | `scripts/test-optional-recipe-removal.mjs` | 아니오 |
|
||||
| file/IndexedDB/OPFS/Cache 심층 계약 | `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` | 아니오 |
|
||||
| browser data current-status ledger | `docs/architecture/browser-data-capability-completion-ledger.md` | 문서만 |
|
||||
| 심층 설계/ADR | `docs/architecture/browser-file-and-origin-storage.md`, `decisions/VD-11-browser-file-and-origin-storage.md` | 문서만 |
|
||||
| client cache scope/persistence 설계 | `docs/architecture/client-cache-and-storage.md`, `decisions/VD-13-client-cache-scope-and-persistence.md` | 문서만 |
|
||||
| realtime/Web Push/Polling 설계와 reference runtime | `docs/architecture/realtime-events-web-push-and-bounded-polling.md`, `decisions/VD-28-realtime-events-web-push-and-bounded-polling.md`, `src/adapters/realtime`, `src/adapters/web-push` | composition 전에는 tree-shaken |
|
||||
| transfer/CDN 설계/ADR | `docs/architecture/presigned-transfer-and-image-cdn.md`, `decisions/VD-12-presigned-transfer-and-image-cdn.md` | 문서만 |
|
||||
| Range/background 결정 | `docs/architecture/decisions/VD-14-resumable-download-and-background-transfer.md` | 문서만 |
|
||||
| storage lifecycle/migration 결정 | `docs/architecture/decisions/VD-15-origin-storage-lifecycle-and-migration.md` | 문서만 |
|
||||
| transfer composition/Image provider 결정 | `docs/architecture/decisions/VD-16-browser-transfer-composition-and-image-delivery.md` | 문서만 |
|
||||
| REST/GraphQL/Connect/gRPC-Web·Protobuf/REST Gateway·Schema·Mapper·Server State 설계 | `docs/architecture/api-contract-schema-mapper-and-server-state.md`, `docs/architecture/protobuf-browser-transport-and-rest-gateway.md`, `decisions/VD-23-api-transport-selection-and-rest-execution.md`, `decisions/VD-24-runtime-schema-and-boundary-mapper.md`, `decisions/VD-25-server-state-cache-lifecycle.md`, `decisions/VD-26-persisted-graphql-operation.md`, `decisions/VD-27-grpc-web-unary-and-server-stream.md`, `decisions/VD-29-connect-web-and-browser-protobuf-runtime.md`, `decisions/VD-30-protobuf-contract-and-rest-gateway.md` | 문서만 |
|
||||
| provider-neutral Browser RPC V3 계약/port/runtime | `src/contracts/browser-rpc.ts`, `src/application/ports/browser-rpc`, `src/adapters/browser-rpc`, `tests/unit/browser-rpc` | composition 전에는 tree-shaken |
|
||||
| API contract/server-state 복구 runbook | `docs/operations/api-contract-and-server-state-recovery.md` | 문서만 |
|
||||
| storage 복구 runbook template | `docs/operations/browser-file-storage-recovery.md` | 문서만 |
|
||||
| transfer/CDN 복구 runbook template | `docs/operations/browser-transfer-recovery.md` | 문서만 |
|
||||
| browser-native reference runtime | `src/adapters/browser-files`, `src/adapters/browser-transfer`, `src/adapters/storage/indexeddb`, `src/adapters/storage/opfs`, `src/adapters/cache-storage` | composition 전에는 tree-shaken |
|
||||
| reference runtime browser evidence | `tests/browser-capabilities` | 아니오 |
|
||||
| 정적/번들 gate | `scripts/check-optional-recipes.ts` | build 도구 |
|
||||
| negative fixture | `scripts/check-optional-recipe-fixtures.ts` | 아니오 |
|
||||
| 완전 제거 gate | `scripts/test-optional-recipe-removal.ts` | 아니오 |
|
||||
| native reference runtime 제거 gate | `scripts/test-browser-file-storage-runtime-removal.ts` | 아니오 |
|
||||
|
||||
현재 `productionRuntimeDependencies`는 빈 배열이며 12개 recipe 모두 선택되지
|
||||
않았다. TypeScript example은 product source가 import할 library가 아니라 선택
|
||||
시 복사하고 좁힐 출발점이다.
|
||||
않았다. `recipes/`의 TypeScript example은 선택 시 복사하고 좁힐 출발점이고,
|
||||
`src/adapters`의 browser-native reference runtime은 공통 lifecycle·failure
|
||||
mechanism을 재사용할 수 있는 실제 구현이다. 제품 feature는 이 runtime에
|
||||
schema/codec/query와 dataset 정책을 주입하고 더 좁은 facade 뒤에서 조립한다.
|
||||
|
||||
### Reference runtime bundle budget
|
||||
|
||||
`referenceRuntime`이 있는 recipe는 catalog의 `sourceRoots`를 실제 budget entry로
|
||||
사용한다. gate는 중첩 root의 실행 가능한 source를 중복 제거하고 경로순으로
|
||||
정렬한 뒤, 모든 module을 하나의 synthetic entry에 포함한다. 이 entry는 Vite
|
||||
production mode, ES2022/ES module, esbuild minify로 build하며 tree-shaking을
|
||||
명시적으로 끈다. 따라서 synthetic consumer가 호출 여부를 알 수 없다는 이유로
|
||||
validation, quota, integrity, cleanup 같은 fail-closed guard가 예산에서 빠지지
|
||||
않는다. 생성된 모든 chunk/asset의 Node zlib gzip byte 합계를 catalog의
|
||||
`bundleBudgetGzipBytes`와 비교하고 결과와 SHA-256을
|
||||
`artifacts/quality/optional-recipes.json`에 기록한다.
|
||||
|
||||
이 synthetic build는 설치 크기 상한을 검증하기 위한 것이며 product bootstrap에
|
||||
runtime을 compose하지 않는다. 별도의 production manifest/module-inventory
|
||||
검사는 선택되지 않은 runtime source가 실제 `dist`에 없는지 계속 검증한다.
|
||||
|
||||
2026-07-28 기준 동일 설정의 `offline-indexeddb` 실측은 32,930 gzip bytes였다.
|
||||
기존 8,000 bytes 값은 bundle 측정 없이 선언된 값으로 실제 reference runtime과
|
||||
일치하지 않아 약 9% headroom을 둔 36,000 bytes로 교정했다.
|
||||
`service-worker-pwa` 10,000 bytes와 `file-transfer` 52,000 bytes는 현재 실측을
|
||||
수용하므로 유지한다. 이후 source가 예산을 넘으면 gate를 우회하거나 예산을
|
||||
자동 인상하지 않고, output artifact와 변경 이유를 검토해야 한다.
|
||||
|
||||
## 2. 어느 경계에 두는가
|
||||
|
||||
@@ -40,10 +89,10 @@ type은 facade 밖으로 노출하지 않는다.
|
||||
|
||||
| recipe | 설치하는 경우 | 설치하면 안 되는 경우 | 핵심 fallback |
|
||||
| --- | --- | --- | --- |
|
||||
| realtime | ordered push/resume protocol이 확정됨 | polling이 충분하거나 ordering owner 없음 | bounded polling/stale UI |
|
||||
| offline/IndexedDB | durable offline data/queue가 제품 요구 | credential 저장, DB 직접 연결, HTTP cache로 충분 | online-only + offline state |
|
||||
| Service Worker/PWA | install/offline shell과 cache owner 승인 | update/rollback UX 없음 | hosting cache 기반 network app |
|
||||
| file transfer | progress/cancel/size/type 정책 필요 | long-lived credential URL | bounded normal request |
|
||||
| realtime | foreground ordered event 또는 duplex protocol이 확정됨 | focus refetch/manual refresh가 충분하거나 ordering·replay owner 없음 | bounded polling 또는 stale UI |
|
||||
| offline/IndexedDB/OPFS | structured offline data/queue 또는 large local binary가 제품 요구 | credential, partition/retention/recovery 미정, HTTP cache로 충분 | read-only/online-only 또는 승인된 bounded Blob |
|
||||
| Service Worker/Cache Storage | install/offline shell 또는 public HTTP representation cache owner 승인 | auth/private/opaque cache, update/rollback UX 없음 | hosting cache 기반 network app |
|
||||
| file/picker/download | selection/preview/upload/download와 bounded memory/integrity 요구 | backend 재검증 없음, whole-buffer large file, long-lived credential URL | native input + authorized direct download |
|
||||
| generated API | versioned source와 drift CI가 있음 | DTO가 domain/UI로 노출됨 | typed request builder + schema |
|
||||
| feature flag | rollout/kill switch owner와 default 있음 | authorization에 사용 | typed local default |
|
||||
| Web Worker | profiler가 main-thread 병목을 증명 | 단순 network I/O | chunked/deferred execution |
|
||||
@@ -57,6 +106,13 @@ type은 facade 밖으로 노출하지 않는다.
|
||||
SSOT다. 문서와 catalog가 다르면 gate가 검사하는 catalog를 우선 고치고 이 표도
|
||||
같이 갱신한다.
|
||||
|
||||
Web Push target은 현재 13번째 설치 recipe가 아니다. 기존 `realtime`,
|
||||
`service-worker-pwa`, `browser-permission`의 인접 경계를 조합해야 하는 별도
|
||||
`NOT_SELECTED` capability다. 실제 선택 전 VD-10 amendment와 machine-readable
|
||||
catalog에 permission, subscription/backend provider, worker handler, notification
|
||||
policy와 removal source를 명시하며, foreground realtime이 선택됐다는 이유로
|
||||
Web Push를 함께 설치하지 않는다.
|
||||
|
||||
## 4. 공통 구현 순서
|
||||
|
||||
1. 문제를 vendor 이름이 아닌 capability와 측정값으로 기록한다.
|
||||
@@ -86,19 +142,67 @@ SSOT다. 문서와 catalog가 다르면 gate가 검사하는 catalog를 우선
|
||||
resume token expiry를 정의한다.
|
||||
- duplicate/out-of-order는 domain use case에 전달하기 전에 정책화한다.
|
||||
- route unmount/logout에서 unsubscribe하고 heartbeat timer를 종료한다.
|
||||
- SSE는 active document의 one-way stream, WebSocket은 duplex protocol,
|
||||
Web Push는 Service Worker가 받는 background notification hint, Polling은
|
||||
visible/finite HTTP scheduling policy로 분리한다.
|
||||
- 기본 event 효과는 query namespace invalidation과 authoritative refetch다.
|
||||
gap, cursor expiry와 queue overflow에서는 delta 적용을 중단하고 snapshot으로
|
||||
복구한다.
|
||||
- Web Push permission은 user action에서만 요청하고 subscription endpoint/key와
|
||||
payload를 storage, URL, telemetry나 application state에 노출하지 않는다.
|
||||
- detailed status, target contract와 구현 work package는
|
||||
[Realtime events, Web Push, and bounded polling](./realtime-events-web-push-and-bounded-polling.md)을
|
||||
따른다. 현재 concrete runtime과 product selection은 없다.
|
||||
|
||||
### Offline/Service Worker
|
||||
### IndexedDB/OPFS
|
||||
|
||||
- store/cache 이름과 schema는 release와 독립적인 migration version을 가진다.
|
||||
- quota, corrupt row, partial migration, downgrade/rollback을 fixture로 만든다.
|
||||
- authenticated response와 credential은 기본 cache 대상이 아니다.
|
||||
- stale worker loop를 막고 unregister 후 owned cache 삭제가 가능한지 검증한다.
|
||||
- 기존 동기식 preference `StoragePort`에 넣지 않고 feature-specific async
|
||||
repository와 large-object port를 사용한다.
|
||||
- raw database/transaction/store/index/schema version은 adapter 밖으로 노출하지
|
||||
않는다. request success가 아니라 transaction complete 이후에만 성공이다.
|
||||
- DDL schema와 record codec version을 분리하고 schema upgrade는 additive,
|
||||
data migration은 resumable bounded batch로 실행한다.
|
||||
- versionchange/blocked/future schema를 read-only/online-only 상태로 드러내며
|
||||
자동 reload와 database 삭제를 금지한다.
|
||||
- OPFS는 immutable bytes만 소유하고 IndexedDB journal이
|
||||
`PREPARING -> FILES_READY -> COMMITTED -> CLEANED` commit authority를 가진다.
|
||||
- quota, corrupt row/manifest, migration checkpoint, worker crash, storage eviction,
|
||||
N-1 rollback을 fixture와 실제 browser에서 검증한다.
|
||||
|
||||
### File/generated API
|
||||
### Cache Storage/Service Worker
|
||||
|
||||
- upload는 client MIME을 신뢰하지 않고 size/type/server rejection을 모두 다룬다.
|
||||
- Cache Storage는 public HTTP representation 전용이며 application repository나
|
||||
query cache가 아니다.
|
||||
- auth, cookie-dependent, private, personal, no-store, opaque, redirect, 206을
|
||||
거절하고 query/Vary exact match를 보존한다.
|
||||
- candidate 전체의 type/size/integrity가 검증된 뒤에만 release를 활성화하고
|
||||
verified previous release를 rollback용으로 유지한다.
|
||||
- stale worker loop를 막고 unregister와 parsed owned-cache cleanup을 별도 lifecycle로
|
||||
검증한다.
|
||||
|
||||
### File/Blob/picker/download, presigned transfer와 generated API
|
||||
|
||||
- native File/Blob/handle은 transient adapter vault 안에 두고 application에는
|
||||
opaque ref, untrusted metadata와 bounded range/chunk만 전달한다.
|
||||
- native input을 baseline으로 두고 system picker는 user activation 안에서만
|
||||
progressive enhancement한다. dismissal은 failure가 아니다.
|
||||
- upload는 client MIME을 신뢰하지 않고 count/size/signature/server rejection,
|
||||
resumable session, part checksum/idempotency, quarantine을 다룬다.
|
||||
- presigned URL은 application에 raw URL로 노출하지 않고 in-memory identity
|
||||
capability로 보관한다. method/resource 또는 session-part/offset/length/checksum,
|
||||
expiry, origin/path/query/header를 정확히 묶고 data-plane fetch는 credential,
|
||||
redirect, referrer와 cache를 fail-closed 정책으로 제한한다.
|
||||
- resume는 IndexedDB checkpoint만 신뢰하지 않고 server status와 다시 선택한
|
||||
source의 part digest를 대조한다. checkpoint에는 URL/query/signed header/token을
|
||||
저장하지 않으며 explicit abort가 불명확하면 reconcile 전까지 유지한다.
|
||||
- progress는 unknown total을 허용하며 navigation/unmount에서 AbortSignal로
|
||||
취소한다.
|
||||
취소한다. server upload session은 별도 abort/TTL cleanup이 필요하다.
|
||||
- 큰 download는 single `Uint8Array`/Blob이 아니라 browser handoff 또는
|
||||
backpressure stream을 사용하고 handoff와 confirmed save를 구분한다.
|
||||
- Image CDN은 raw transform URL builder가 아니라 opaque asset과
|
||||
composition-registered preset으로만 responsive descriptor를 만든다. immutable
|
||||
revision, format/width/pixel/decode/cache/expiry와 CDN origin을 검증한다.
|
||||
- object URL은 explicit lease로 만들고 replacement/unmount에서 revoke한다.
|
||||
- generated code는 facade 뒤 DTO이며 runtime response schema와 contract drift
|
||||
gate를 유지한다.
|
||||
|
||||
@@ -128,17 +232,22 @@ SSOT다. 문서와 catalog가 다르면 gate가 검사하는 catalog를 우선
|
||||
```bash
|
||||
corepack pnpm check:types:recipes
|
||||
corepack pnpm test:recipes
|
||||
corepack pnpm test:browser-capabilities
|
||||
corepack pnpm build
|
||||
corepack pnpm check:optional-recipes
|
||||
corepack pnpm check:optional-recipe-fixtures
|
||||
corepack pnpm test:optional-recipe-removal
|
||||
corepack pnpm test:browser-file-storage-removal
|
||||
```
|
||||
|
||||
negative gate는 cleanup 누락, unselected dependency, local adapter 밖 vendor
|
||||
import, credential localStorage/URL/telemetry 경로, workflow store의 server-state
|
||||
복제와 production source의 recipe import를 거절한다. removal gate는 recipe와
|
||||
recipe test를 삭제한 임시 사본에서 base typecheck, architecture, test와 build를
|
||||
실행한다.
|
||||
복제, production source의 recipe import와 선택 전 reference runtime composition을
|
||||
거절한다. removal gate는 recipe와 recipe test를 삭제한 임시 사본에서 base
|
||||
typecheck, architecture, test와 build를 실행한다.
|
||||
별도 native-runtime removal gate는 browser file/storage source와 전용 test,
|
||||
catalog metadata를 제거한 임시 사본에서 typecheck, architecture, 전체 base test,
|
||||
build와 optional catalog 검사를 다시 실행한다.
|
||||
|
||||
## 7. 제거 체크리스트
|
||||
|
||||
|
||||
@@ -18,17 +18,18 @@ flowchart LR
|
||||
Contracts --> Presentation
|
||||
```
|
||||
|
||||
The intended dependency rule points inward: presentation calls application use
|
||||
The enforced dependency rule points inward: presentation calls application use
|
||||
cases, adapters implement application ports, and only the composition root
|
||||
selects concrete adapters. Contract registries are the intended named source
|
||||
for routes, API operations, environment values, storage keys, errors, queries,
|
||||
telemetry, and release tokens. The platform review below records where the
|
||||
current runtime still bypasses that target or duplicates registry metadata.
|
||||
selects concrete adapters. Contract registries are the named source for routes,
|
||||
API operations, environment values, storage keys, errors, queries, telemetry,
|
||||
and release tokens. Installed feature contributions extend those registries
|
||||
without making the generic application or router import a concrete feature
|
||||
implementation.
|
||||
|
||||
In ports-and-adapters terms, `presentation` is the current inbound adapter and
|
||||
`adapters` contains the current outbound implementations. The target design
|
||||
makes this role explicit, introduces application input ports, and prevents the
|
||||
React tree from receiving raw outbound dependencies:
|
||||
`adapters` contains the current outbound implementations. The production
|
||||
composition exposes an application input API to React while keeping concrete
|
||||
output ports inside application closures:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
@@ -41,21 +42,38 @@ flowchart LR
|
||||
Bootstrap2 -. selects and injects .-> Outbound
|
||||
```
|
||||
|
||||
The current executable route tree is mounted only after runtime configuration
|
||||
and release-manifest coherence pass. It receives the composed query client,
|
||||
credential-opaque session port, storage port, telemetry port, and immutable
|
||||
build ID. Generic starter pages do not depend on the removable reference
|
||||
feature.
|
||||
The executable route tree is mounted only after runtime configuration and
|
||||
release-manifest coherence pass. `ApplicationProvider` receives the composed
|
||||
application API; concrete session, storage, telemetry, diagnostics, and release
|
||||
ports are not returned to feature pages. TanStack Query is isolated behind the
|
||||
presentation query adapter, while its provider remains React infrastructure.
|
||||
The module-augmented feature registry gives each installed feature a closed ID
|
||||
and exact input type.
|
||||
|
||||
This describes the current starter composition, not the completed target. The
|
||||
capability review found that raw outbound capabilities still reach the React
|
||||
tree, the composed application facade is not yet its entry point, and several
|
||||
route, HTTP, recovery, telemetry, and reference-feature removal contracts are only
|
||||
partially connected. Use the following documents for the evidence and migration
|
||||
plan:
|
||||
Route contracts/codecs, the route-input provider, and lazy runtime modules are
|
||||
separate modules so feature pages do not import the router that loads them.
|
||||
Expected application failures use the shared `Result`/`AppFailure` contract.
|
||||
The architecture gate analyzes TS/TSX imports independently of
|
||||
dependency-cruiser, rejects local JavaScript-family specifiers and executable
|
||||
JavaScript-family files, and fails on unresolved imports, parse failures,
|
||||
forbidden layer edges, or cycles. The remaining project-owned work includes
|
||||
selecting and verifying real hosting, identity, telemetry, vulnerability and
|
||||
signing providers, plus backend/provider/browser/operations evidence for each
|
||||
selected optional capability; the repository does not fabricate that evidence.
|
||||
Use the following
|
||||
documents for implementation details and project integration work:
|
||||
|
||||
- [Frontend platform capability review](./frontend-platform-capability-review.md)
|
||||
- [Frontend ports, adapters, and boundaries](./frontend-ports-adapters-and-boundaries.md)
|
||||
- [API contract, Schema, Mapper, and Server State](./api-contract-schema-mapper-and-server-state.md)
|
||||
- [Protobuf browser transports and REST Gateway](./protobuf-browser-transport-and-rest-gateway.md)
|
||||
- [Backend API and Server State handoff contract](./backend-api-and-server-state-contract.md)
|
||||
- [TypeScript, state, and data flow](./typescript-state-and-data-flow.md)
|
||||
- [Routing, pages, and patterns](./routing-pages-and-patterns.md)
|
||||
- [Browser data capability completion ledger](./browser-data-capability-completion-ledger.md)
|
||||
- [Browser file and origin storage](./browser-file-and-origin-storage.md)
|
||||
- [Client cache and storage](./client-cache-and-storage.md)
|
||||
- [Realtime events, Web Push, and bounded polling](./realtime-events-web-push-and-bounded-polling.md)
|
||||
- [Presigned transfer and Image CDN](./presigned-transfer-and-image-cdn.md)
|
||||
- [Server file capability infrastructure](./server-file-capability-infrastructure.md)
|
||||
- [Frontend platform implementation roadmap](./frontend-platform-implementation-roadmap.md)
|
||||
|
||||
@@ -0,0 +1,757 @@
|
||||
# Presigned transfer, resumable upload, streaming download and Image CDN
|
||||
|
||||
이 문서는 presigned URL, multipart/resumable upload, streaming download와
|
||||
Image CDN을 브라우저 애플리케이션에 넣을 때의 control plane/data plane 경계,
|
||||
무결성, 재개, 만료, 캐시와 복구 계약을 정의한다.
|
||||
|
||||
현재 구현된 개별 reference runtime은 실제 browser `fetch`와 bounded byte
|
||||
stream을 사용한다. 제품별 endpoint, bucket, CDN vendor, asset schema와
|
||||
authorization owner는 아직 조합하지 않는다. 이 개별 runtime은
|
||||
`AVAILABLE_NOT_COMPOSED`이고, Range/composition/provider 같은 후속 delta는 아래
|
||||
표처럼 다른 상태다. 어느 경로도 임의의 URL이나 object key를 application caller가
|
||||
직접 전달하는 범용 HTTP facade가 아니다.
|
||||
|
||||
## 0. 현재 구현과 목표 상태
|
||||
|
||||
이 문서에서 “설계됨”, “reference source가 있음”, “제품에 조합됨”과 “target
|
||||
browser에서 보장 가능함”은 서로 다른 사실이다. primary current status는
|
||||
`NOT_SELECTED`, `DESIGNED_NOT_IMPLEMENTED`, `AVAILABLE_NOT_COMPOSED`, `COMPOSED`,
|
||||
`PLATFORM_LIMITED` 다섯 값 중 하나만 사용한다. production evidence와 traffic
|
||||
admission은 이 status와 별도 축이다.
|
||||
|
||||
| capability | primary current status | 현재 있는 것 | 남은 목표 |
|
||||
| --- | --- | --- | --- |
|
||||
| whole-object Presigned GET/part PUT | `AVAILABLE_NOT_COMPOSED` | strict BFF response validation, in-memory identity vault, bounded GET/PUT executor | top-level wire version, 실제 endpoint/auth/revocation/provider contract |
|
||||
| multipart/resumable upload | `AVAILABLE_NOT_COMPOSED` | server-authoritative session flow, part retry, IDB CAS checkpoint, Web Lock, cross-tab abort signal | non-destructive pause, safe inventory/retention sweep, 실제 BFF/storage/scan |
|
||||
| whole-object foreground streaming download | `AVAILABLE_NOT_COMPOSED` | `200` stream, length/SHA-256, picker save, bounded object URL와 browser handoff mechanism | strategy selector, browser-managed capability issuer와 제품 save UX |
|
||||
| Range resumable download | `DESIGNED_NOT_IMPLEMENTED` | VD-14 production design | port/runtime/checkpoint, `206/200/412/416`, seek/truncate 또는 OPFS staging |
|
||||
| Image CDN policy/verification engine | `AVAILABLE_NOT_COMPOSED` | opaque asset/preset, responsive descriptor, P-256, static-image probe | descriptor HTTP provider/refresh, renderer, 실제 BFF/CDN |
|
||||
| app-managed background download | `NOT_SELECTED` | VD-14 경계와 금지 조건 | 제품이 별도 선택한 지원 browser에서만 optional 구현 |
|
||||
| cross-browser background-download guarantee | `PLATFORM_LIMITED` | browser-managed handoff fallback | 공통 baseline으로 구현 완료를 선언하지 않음 |
|
||||
| app-managed background upload | `NOT_SELECTED` | pause/foreground resume와 명시적으로 분리 | durable source staging/worker auth를 가진 별도 protocol |
|
||||
| cross-browser background-upload guarantee | `PLATFORM_LIMITED` | foreground checkpoint resume fallback | worker lifetime/source permission을 공통 보장하지 않음 |
|
||||
|
||||
현재 runtime은 테스트 전용 mock이 아니라 실제 browser API를 호출하지만, 위
|
||||
`남은 목표`가 구현됐다는 뜻은 아니다. 특히 foreground whole-object streaming
|
||||
증거를 Range resume나 app-managed background download 증거로 재사용하지 않는다.
|
||||
Range와 background download의 상세 상태 머신, destination, fallback, rollout과 완료 기준은
|
||||
[VD-14](./decisions/VD-14-resumable-download-and-background-transfer.md)가
|
||||
소유한다. top-level runtime composition, account teardown, Image descriptor
|
||||
provider/refresh와 safe presentation projection은
|
||||
[VD-16](./decisions/VD-16-browser-transfer-composition-and-image-delivery.md)이
|
||||
소유한다.
|
||||
|
||||
## 1. 경계와 topology
|
||||
|
||||
```text
|
||||
feature use case
|
||||
-> feature-specific transfer facade
|
||||
-> BFF/Web API control plane
|
||||
- authorization
|
||||
- upload session / download capability
|
||||
- resource metadata and lifecycle authority
|
||||
-> browser transfer data plane
|
||||
- capability validation
|
||||
- bounded fetch / stream / part transfer
|
||||
- integrity and cancellation
|
||||
-> object/file storage or CDN
|
||||
- bytes only
|
||||
|
||||
image use case
|
||||
-> ImageDeliveryPort
|
||||
-> backend/CDN-issued immutable asset descriptor
|
||||
-> policy-validated responsive candidates
|
||||
-> presentation-safe <picture>/<img> attributes
|
||||
```
|
||||
|
||||
브라우저는 S3, MinIO, GCS, Azure Blob의 관리자 credential, bucket policy, object
|
||||
key 생성 규칙이나 signing key를 소유하지 않는다. control plane이 짧은 수명의
|
||||
제한된 capability를 발급하고, data plane은 그 capability가 묶은 정확한
|
||||
operation과 bytes만 전송한다.
|
||||
|
||||
## 2. 공통 capability 규칙
|
||||
|
||||
Presigned URL은 단순 URL이 아니라 bearer capability다. 구현은 적어도 다음
|
||||
binding을 하나의 immutable snapshot으로 검증해야 한다.
|
||||
|
||||
- protocol/version과 opaque capability ID
|
||||
- `DOWNLOAD` 또는 특정 upload session/part operation
|
||||
- logical resource/session ID
|
||||
- exact HTTP method
|
||||
- HTTPS와 composition allowlist에 속한 origin/path
|
||||
- capability가 발급한 exact query와 signed request headers
|
||||
- expected media type, byte range 또는 exact part offset/length
|
||||
- hard maximum bytes와 선택한 checksum algorithm/digest
|
||||
- upload 성공 response의 exact status, `Content-Length`,
|
||||
`expectedResponseByteLength`와 opaque receipt header
|
||||
- 발급·만료 시각과 composition의 더 짧은 lifetime ceiling
|
||||
- single-use가 필요한 경우 원자적인 server/provider consumption
|
||||
|
||||
### 2.1 Wire version 목표
|
||||
|
||||
현재 multipart DTO는 `PRESIGNED_MULTIPART_V1`을 exact하게 포함하지만, whole-object
|
||||
presigned capability response에는 top-level transfer protocol literal이 아직
|
||||
없다. strict exact-key schema만으로 현재 payload drift는 막지만, 호환되지 않는
|
||||
wire 변경과 구 client drain을 명시적으로 운영하기에는 부족하다.
|
||||
|
||||
향후 presigned control-plane envelope에는 다음 literal을 추가한다.
|
||||
|
||||
```text
|
||||
protocol = PRESIGNED_TRANSFER_V1
|
||||
```
|
||||
|
||||
- request/response, vault registration과 executor consumption에서 exact match한다.
|
||||
- 알 수 없는 값, 누락과 newer version은 fail-closed한다.
|
||||
- Range는 이 literal에 암묵적으로 섞지 않고
|
||||
`RANGE_RESUMABLE_DOWNLOAD_V1` 별도 protocol을 사용한다.
|
||||
- Image descriptor HTTP envelope도 `IMAGE_CDN_DESCRIPTOR_V1`로 versioning한다.
|
||||
- version을 추가하기 전까지 현재 구현 상태는 계속
|
||||
`AVAILABLE_NOT_COMPOSED`이며 wire-version 목표가 구현됐다고 기록하지 않는다.
|
||||
|
||||
URL의 query와 signed headers는 credential material로 취급한다. persistence,
|
||||
checkpoint, diagnostics, analytics, error message, referrer와 application state에
|
||||
넣지 않는다. runtime caller가 URL/query/header를 조립하거나 capability의
|
||||
method, origin, byte limit과 expiry를 늘릴 수 없다.
|
||||
|
||||
Data-plane `fetch` 기본값은 다음과 같다.
|
||||
|
||||
```text
|
||||
credentials = omit
|
||||
redirect = error
|
||||
referrerPolicy = no-referrer
|
||||
cache = no-store
|
||||
mode = same-origin or explicitly approved CORS
|
||||
```
|
||||
|
||||
cross-origin object storage/CDN은 exact origin allowlist, CORS method/header,
|
||||
노출할 response header와 CSP `connect-src`/`img-src` 계약이 있어야 한다.
|
||||
redirect를 따라가며 signed query를 다른 origin으로 전달하지 않는다.
|
||||
|
||||
## 3. Presigned URL
|
||||
|
||||
### 3.1 Control plane
|
||||
|
||||
실제 발급은 feature-specific BFF adapter가 소유한다. composition은 presigned
|
||||
capability 발급 endpoint를 HTTPS absolute URL 하나로 고정해 factory에 한 번
|
||||
주입한다. application caller는 endpoint를 고르거나 path/query를 조립할 수 없다.
|
||||
공통 runtime은 이 고정 endpoint와 strict envelope만 알고 provider-specific
|
||||
signing DTO는 알지 않는다.
|
||||
|
||||
권장 흐름:
|
||||
|
||||
```text
|
||||
browser -> authenticated BFF capability request
|
||||
BFF -> resource/session authorization and metadata lookup
|
||||
BFF -> object service signer
|
||||
BFF -> opaque bound capability
|
||||
browser -> validated direct data-plane request
|
||||
```
|
||||
|
||||
발급 endpoint의 `2xx`만으로 authorization을 추론하지 않는다. runtime schema가
|
||||
전체 capability를 검증하고, data request 시에도 object storage/BFF가 method,
|
||||
expiry, size/checksum 조건을 강제해야 한다. presigned URL은 underlying credential
|
||||
revocation이나 server policy 때문에 표기된 expiry보다 일찍 무효화될 수 있다.
|
||||
|
||||
upload part capability 요청에서 전달되는 `requestBindingSha256`,
|
||||
`uploadBindingSha256`와 part digest는 authorization proof가 아니다.
|
||||
`UPLOAD_PART` capability binding은 `protocol: PRESIGNED_MULTIPART_V1`을 exact하게
|
||||
포함한다. BFF는 `sessionId`로 server-owned session을 다시 읽고 protocol,
|
||||
subject/purpose/state/expiry와 part plan을 authorization한 뒤 canonical binding을
|
||||
직접 재계산해야 한다. client digest를 그대로 신뢰하거나 단순 echo해서 signed
|
||||
URL을 발급하면 안 된다.
|
||||
|
||||
### 3.2 Consumption
|
||||
|
||||
- capability를 async 대기하는 동안 caller 입력과 dependency method를 snapshot한다.
|
||||
- 사용 전에 만료뿐 아니라 최소 잔여 lifetime도 검사한다.
|
||||
- 만료/403은 임의 retry가 아니라 control plane의 새 capability 발급으로 복구한다.
|
||||
- ambiguous network failure 뒤 upload part를 새 bytes로 덮어쓰지 않는다.
|
||||
- client-side single-use map은 UX 최적화일 뿐이다. cross-tab/replay authority는
|
||||
server 또는 composition-owned atomic consumer다.
|
||||
- signed response의 URL, query, raw header와 exception text를 관측성에 남기지 않는다.
|
||||
|
||||
## 4. Multipart/resumable upload
|
||||
|
||||
이 reference runtime의 기본 모델은
|
||||
`PRESIGNED_MULTIPART_V1` capability 기반 ordered multipart protocol이다. 이
|
||||
literal은 모든 upload control-plane request/response, session과 durable
|
||||
checkpoint에서 일치해야 하며 다른 값이나 누락은 fail-closed한다. 특정 S3 DTO를
|
||||
application port로 노출하지 않으며 tus 같은 offset protocol을 선택하면 별도
|
||||
protocol/version과 wire adapter가 동일한 상위 session contract를 구현한다.
|
||||
|
||||
### 4.1 Session contract
|
||||
|
||||
Control plane이 소유하는 operation:
|
||||
|
||||
1. `create`: authorization, purpose, declared bytes/media와 source binding을 확인
|
||||
2. `status/list parts`: server-authoritative session/part state 반환
|
||||
3. `issue part capability`: exact session, part number, offset, length와 checksum binding
|
||||
4. `complete`: ordered part receipt와 checksum을 검증
|
||||
5. `abort`: server upload를 중단하고 orphan cleanup을 예약
|
||||
|
||||
browser HTTP adapter는 `CREATE_SESSION`, `GET_STATUS`, `COMPLETE`, `ABORT`의
|
||||
closed operation set을 composition-owned fixed HTTPS endpoint map에 연결한다.
|
||||
모두 bounded `POST application/json`이고 caller가 URL을 제공하지 못한다.
|
||||
upload control endpoint response는 exact URL/status/content type과
|
||||
request/response byte cap, deadline, `Retry-After` ceiling을 검증한다. part
|
||||
capability는 앞 절의 별도 fixed BFF endpoint를 redirect 금지로 사용하고 strict
|
||||
response envelope 및 request/response의 `UPLOAD_PART` binding protocol이
|
||||
`PRESIGNED_MULTIPART_V1`인지 대조한다.
|
||||
|
||||
Session은 적어도 opaque ID, protocol version, exact total bytes, media type,
|
||||
part size, part count, concurrency ceiling, checksum algorithm과 expiry를 묶는다.
|
||||
part number는 1부터 연속적이어야 하고 마지막 part를 제외한 part length는
|
||||
고정한다.
|
||||
|
||||
`PRESIGNED_MULTIPART_V1`의 canonical digest 계약은 다음과 같다. 각 field는
|
||||
아래 순서의 UTF-8 line으로 직렬화하고 SHA-256 lowercase hex를 사용한다.
|
||||
|
||||
```text
|
||||
fingerprint.digestHex =
|
||||
SHA-256(
|
||||
"SHA-256-PARTS-V1"
|
||||
fingerprint.byteLength
|
||||
fingerprint.partSizeBytes
|
||||
fingerprint.partCount
|
||||
"{partNumber}:{offset}:{byteLength}:{checksumSha256}" for each ordered part
|
||||
)
|
||||
|
||||
requestBindingSha256 =
|
||||
SHA-256(
|
||||
"RESUMABLE-UPLOAD-BINDING-V1"
|
||||
uploadKey
|
||||
purpose
|
||||
mediaType
|
||||
fingerprint.algorithm
|
||||
fingerprint.digestHex
|
||||
fingerprint.byteLength
|
||||
fingerprint.partSizeBytes
|
||||
fingerprint.partCount
|
||||
)
|
||||
|
||||
uploadBindingSha256 =
|
||||
SHA-256(
|
||||
"RESUMABLE-UPLOAD-SESSION-BINDING-V1"
|
||||
requestBindingSha256
|
||||
sessionId
|
||||
fingerprint.algorithm
|
||||
fingerprint.digestHex
|
||||
fingerprint.byteLength
|
||||
fingerprint.partSizeBytes
|
||||
fingerprint.partCount
|
||||
)
|
||||
```
|
||||
|
||||
각 괄호 안의 항목은 실제로 줄바꿈 하나로 연결하며 마지막 빈 line은 추가하지
|
||||
않는다. BFF는 create에서 받은 선언을 server policy/session snapshot과 함께
|
||||
보관하고, part capability 발급 때 그 snapshot으로 request/upload digest와 exact
|
||||
part offset/length/checksum/idempotency를 재계산한다. complete에서는 server
|
||||
ledger의 ordered part set으로 fingerprint digest도 다시 계산한다. digest 일치는
|
||||
input binding의 무결성 신호일 뿐 subject authorization, session ownership 또는
|
||||
session state 검사를 대체하지 않는다.
|
||||
|
||||
### 4.2 Browser transfer
|
||||
|
||||
- source는 bounded `readRange(offset, length)`를 제공한다.
|
||||
- part bytes 하나와 digest 계산에 필요한 copy만 메모리에 둔다.
|
||||
- `partSize × concurrency × copyFactor`가 composition memory ceiling을 넘으면
|
||||
시작 전에 거절한다.
|
||||
- 각 part는 exact offset/length와 SHA-256을 계산한 뒤 capability를 발급받는다.
|
||||
- retry는 동일 session/part/offset/length/checksum/idempotency binding에만 허용한다.
|
||||
- 429/모든 `5xx`/network retry는 composition의 bounded attempt, bounded
|
||||
`Retry-After`와 abortable backoff 안에서만 수행한다.
|
||||
- 만료/authorization failure는 최대 정책 범위 안에서 capability를 재발급한다.
|
||||
- upload response의 opaque receipt/ETag를 whole-file digest로 해석하지 않는다.
|
||||
- PUT capability는 성공 status, receipt header와
|
||||
`expectedResponseByteLength`를 묶는다. runtime은 exact `Content-Length`를
|
||||
확인하고 response body를 hard cap과 동일 deadline 안에서 끝까지 bounded
|
||||
drain한 뒤에만 receipt를 성공으로 채택한다. `204`는 expected response bytes가
|
||||
`0`일 때만 허용하고 `Content-Length` 부재를 0으로 정규화한다.
|
||||
- complete 전 server-authoritative status와 local receipt를 reconcile한다.
|
||||
- complete 성공은 scan 완료가 아니라 `QUARANTINED`다.
|
||||
|
||||
### 4.3 Resume와 checkpoint
|
||||
|
||||
Checkpoint에는 다음만 저장할 수 있다.
|
||||
|
||||
- schema version, exact `PRESIGNED_MULTIPART_V1`, revision과 lifecycle state
|
||||
- opaque upload key, session ID와 `requestBindingSha256`
|
||||
- `SHA-256-PARTS-V1` fingerprint의 total bytes, part size/count와 digest
|
||||
- session expiry와 server concurrency ceiling
|
||||
- 완료 part의 number/offset/length/checksum/opaque receipt
|
||||
- 마지막 reconciliation 시각
|
||||
|
||||
Presigned URL, query, signed header, bearer token, file name, local path, account ID와
|
||||
raw backend error는 저장하지 않는다.
|
||||
위에 명시한 SHA-256 fingerprint/part checksum과 bounded opaque part receipt는
|
||||
서버 reconcile에 필요한 비권한성 checkpoint field이므로 예외적으로 해당 account
|
||||
partition에만 보존한다. raw provider ETag를 임의로 저장하는 것이 아니며 이
|
||||
필드들도 diagnostics, telemetry, ticket 또는 application-facing 결과에는
|
||||
노출하지 않는다.
|
||||
|
||||
resume 시 checkpoint만 신뢰하지 않는다.
|
||||
|
||||
1. 사용자가 다시 선택한 source의 exact byte length와 source binding을 검사한다.
|
||||
2. server status/list-parts를 authoritative하게 읽는다.
|
||||
3. 완료되었다고 주장하는 각 part의 local bytes를 다시 bounded hash하여
|
||||
server checksum/receipt와 대조한다.
|
||||
4. 불일치하면 해당 session을 complete하지 않고 abort/restart 또는 사용자 복구로
|
||||
전환한다.
|
||||
5. 새 part만 업로드한 뒤 전체 ordered set을 다시 reconcile한다.
|
||||
|
||||
`GET_STATUS`가 HTTP `404` 또는 `410`을 반환하거나 decoded status가
|
||||
`NOT_FOUND`/`EXPIRED`이면 해당 session은 terminal이다. runtime은 CAS revision을
|
||||
확인해 checkpoint를 제거하고 새 session으로 restart하거나
|
||||
`EXPIRED_RESOURCE/RESTART`를 반환한다. 사라진 session의 checkpoint를 다음
|
||||
invocation까지 반복해서 붙잡지 않는다. abort의 `404/410`도 checkpoint를
|
||||
정리하고 application에는 `ORPHANED`로 닫는다.
|
||||
|
||||
한 session은 cross-tab mutation lock으로 직렬화한다. lock은 correctness의 유일한
|
||||
근거가 아니며 server idempotency와 part CAS가 최종 authority다.
|
||||
|
||||
명시적 abort는 같은 runtime의 controller뿐 아니라 strict
|
||||
`RESUMABLE_UPLOAD_CANCEL_V1` BroadcastChannel을 통해 같은 origin의 다른
|
||||
runtime에도 opaque upload key의 ephemeral cancel 신호를 보낸다. 수신 runtime은
|
||||
진행 중 fetch/read/backoff의 AbortSignal을 먼저 중단해 Web Lock을 내보내고,
|
||||
abort 요청 runtime이 lock 안에서 durable checkpoint를 `ABORT_PENDING`으로
|
||||
바꾼 뒤 server abort/reconcile을 실행한다. 이 메시지는 session ID, capability,
|
||||
signed URL이나 receipt를 포함하거나 저장하지 않으며 server state authority가
|
||||
아니다. BroadcastChannel이 없으면 correctness는 유지되지만 abort는 caller가
|
||||
정한 bounded deadline 아래 다른 context의 lock 해제를 기다린다.
|
||||
|
||||
사용자 cancel은 현재 browser work 중지이고 server abort와 다르다. 명시적 abort를
|
||||
요청했는데 결과가 불명확하면 checkpoint를 즉시 성공으로 삭제하지 않고 다음
|
||||
reconcile에서 server 상태를 확인한다. backend는 만료된 orphan multipart를
|
||||
정리하는 TTL job을 가져야 한다.
|
||||
|
||||
application-facing upload 성공값은 `state`, opaque `resourceId`, `byteLength`,
|
||||
`replayed`만 반환한다. control-plane 검증에 사용한 `sessionId`,
|
||||
`requestBindingSha256`와 file fingerprint는 public outcome에 노출하지 않는다.
|
||||
|
||||
### 4.4 아직 구현되지 않은 pause와 checkpoint lifecycle 목표
|
||||
|
||||
현재 `ResumableUploadPort`는 `upload()`와 server-side `abort()`만 제공한다.
|
||||
caller가 자신의 `AbortSignal`을 중단하면 committed checkpoint가 남아 다음
|
||||
`upload()`에서 resume할 수 있지만, 이것은 명시적 pause protocol이 아니다. 다른
|
||||
tab의 active upload를 non-destructive하게 pause하는 API도 없으며 현재 cross-tab
|
||||
cancel signal은 explicit server abort를 준비하기 위한 신호다.
|
||||
|
||||
향후 pause를 선택하면 다음을 별도 version으로 구현한다.
|
||||
|
||||
```text
|
||||
pause(uploadKey)
|
||||
-> active read/fetch/backoff cancel
|
||||
-> RESUMABLE_UPLOAD_PAUSE_V1 ephemeral cross-context signal
|
||||
-> per-key mutation lock
|
||||
-> exact revision CAS to PAUSED
|
||||
-> in-memory part capability retirement
|
||||
-> no server multipart abort
|
||||
|
||||
resume through upload()
|
||||
-> PAUSED checkpoint validation
|
||||
-> server-authoritative status
|
||||
-> local completed-part re-hash
|
||||
-> CAS to ACTIVE
|
||||
-> missing parts only
|
||||
```
|
||||
|
||||
현재 checkpoint `schemaVersion: 1`의 state는 `ACTIVE | ABORT_PENDING`뿐이다.
|
||||
`PAUSED`를 durable state로 추가한다면 unknown-old-writer behavior와 migration을
|
||||
정한 `schemaVersion: 2`가 필요하다. 기존 schema에 필드를 몰래 추가하지 않는다.
|
||||
|
||||
현재 checkpoint store도 single-key `read/CAS/remove`와 account partition 전체
|
||||
삭제만 제공한다. abandoned upload가 같은 `uploadKey`로 다시 열리지 않으면 local
|
||||
checkpoint를 retention 기준으로 자동 발견·정리하지 못한다. 향후 admin lifecycle은
|
||||
다음을 제공한다.
|
||||
|
||||
- account partition 안에서 cursor 기반 bounded safe-summary inventory
|
||||
- maximum age, count와 logical-byte budget
|
||||
- expired/terminal candidate의 server status 재확인
|
||||
- active lock/lease와 `ABORT_PENDING`을 무조건 삭제하지 않는 분류
|
||||
- checkpoint와 owned local staging이 있다면 같은 cleanup journal로 처리
|
||||
- cleanup response loss, blocked database와 CAS conflict reconciliation
|
||||
- logout/account deletion용 exact partition maintenance authority
|
||||
|
||||
inventory는 file name, path, account/resource/session ID, digest, receipt와 capability를
|
||||
application이나 operator UI에 반환하지 않는다. 제품 resume UI가 display metadata를
|
||||
필요로 하면 제품 repository가 opaque `uploadKey`와 별도로 소유한다.
|
||||
|
||||
이 pause/inventory/retention 항목은 현재 **설계 목표이며 구현 완료가 아니다**.
|
||||
app-managed background upload도 이 항목에 포함되지 않는다. worker upload는 source
|
||||
bytes의 durable staging, worker auth/reissue, version drain과 platform support가
|
||||
승인된 별도 optional capability다.
|
||||
|
||||
## 5. Streaming download
|
||||
|
||||
Streaming download는 server resource를 전체 `Blob`/`ArrayBuffer`로 materialize하지
|
||||
않고 `Response.body`를 closed-result byte source로 변환한다.
|
||||
|
||||
- response는 200, non-opaque, non-redirect이고 body가 있어야 한다.
|
||||
- capability의 media type, content encoding 정책과 response header가 일치해야 한다.
|
||||
- native chunk는 configured output chunk ceiling으로 다시 분할한다.
|
||||
- 누적 bytes가 expected/hard maximum을 넘으면 즉시 reader를 cancel한다.
|
||||
- EOF에서 expected bytes보다 작으면 truncated failure다.
|
||||
- integrity-required policy는 vetted incremental verifier를 사용하고 destination
|
||||
close 전에 digest를 확인한다.
|
||||
- first failed chunk 뒤에는 더 이상 bytes를 노출하지 않는다.
|
||||
- abort/consumer early return에서 reader를 cancel하고 lock/capability lease를
|
||||
해제한다.
|
||||
- byte source는 기본 one-shot이며 두 번째 stream 소비를 거절한다.
|
||||
|
||||
File System Access save picker가 있으면 user activation 안에서 destination을 먼저
|
||||
열고 stream을 쓴다. close와 integrity verification이 끝난 경우만 `SAVED`다.
|
||||
anchor/navigation은 `BROWSER_HANDOFF`이며 disk write 완료를 의미하지 않는다.
|
||||
save picker가 없는 browser의 whole-Blob fallback은 별도 small-artifact hard cap
|
||||
아래에서만 허용한다.
|
||||
|
||||
Range 기반 resumable download는 이 streaming contract와 다른 capability다.
|
||||
도입하려면 validator-bound `Range`, `206 Content-Range`, destination seek/truncate,
|
||||
ETag/If-Range와 final whole-object integrity를 별도 계약으로 추가한다. 단순히
|
||||
partial bytes를 기존 파일 뒤에 append하지 않는다.
|
||||
|
||||
현재 presigned provider는 `Range` request header를 금지하고 GET success를
|
||||
`200`으로 고정하며 executor는 `Content-Range`를 거절한다. save destination도
|
||||
순차 writable만 제공한다. 따라서 이 문단은 구현 설명이 아니라 미구현 경계를
|
||||
뜻한다.
|
||||
|
||||
목표 `RANGE_RESUMABLE_DOWNLOAD_V1`은 다음을 모두 포함한다.
|
||||
|
||||
- immutable generation 또는 strong validator와 representation binding
|
||||
- 비권한성 account-partitioned checkpoint와 CAS offset
|
||||
- exact `206`, Range 무시/validator mismatch의 `200`, mode가 계약한 `412`,
|
||||
`416` 상태 머신
|
||||
- capability 재발급 뒤 representation binding 재검증
|
||||
- seek/truncate destination 또는 journaled OPFS staging
|
||||
- durable segment commit 뒤에만 checkpoint offset 전진
|
||||
- 완료 뒤 destination 전체 재읽기와 whole-object SHA-256
|
||||
- partial count/byte/age retention, crash/logout/account-switch cleanup
|
||||
- system picker 미지원 환경의 browser-managed handoff/server-generation fallback
|
||||
|
||||
상세 불변조건과 promotion gate는
|
||||
[VD-14](./decisions/VD-14-resumable-download-and-background-transfer.md)를 따른다.
|
||||
Range의 primary current status는 `DESIGNED_NOT_IMPLEMENTED`다.
|
||||
|
||||
### 5.1 아직 구현되지 않은 strategy selector
|
||||
|
||||
현재 download strategy는 composition-registered file policy 하나에
|
||||
`BROWSER_MANAGED`, `PROMPT_AND_STREAM` 또는 `BOUNDED_OBJECT_URL`로 정적으로
|
||||
고정된다. source kind, exact size와 실제 picker/seek/OPFS 지원을 입력으로 안전한
|
||||
fallback을 선택하는 공통 headless selector는 없다.
|
||||
|
||||
목표 selector는 user-agent 문자열이 아니라 capability probe를 사용한다.
|
||||
|
||||
| 조건 | 목표 결정 |
|
||||
| --- | --- |
|
||||
| server file, 탭 종료 뒤 계속 필요 | `BROWSER_MANAGED_HANDOFF` |
|
||||
| verified foreground save, picker 지원 | `WHOLE_OBJECT_PICKER_STREAM` |
|
||||
| Range resume 필수, destination 계약 충족 | `RANGE_RESUMABLE_FOREGROUND` |
|
||||
| 작은 generated artifact | `BOUNDED_OBJECT_URL` |
|
||||
| 큰 generated artifact, picker 미지원 | `SERVER_GENERATION_REQUIRED` 또는 `UNSUPPORTED` |
|
||||
| background download가 선택되고 OPFS만 지원 | `APP_MANAGED_BACKGROUND_DOWNLOAD`; app-private staging 후 foreground export |
|
||||
|
||||
위 값은 application-level execution plan이다. 현재 file-delivery primitive에는
|
||||
VD-16의 고정 mapping으로만 투영한다. source kind
|
||||
`BROWSER_MANAGED_RESOURCE`를 strategy로 사용하거나 `PROMPT_AND_STREAM`을
|
||||
Range plan으로 재사용하지 않는다.
|
||||
|
||||
picker 미지원 때문에 unbounded Blob ceiling을 올리지 않는다. browser-managed
|
||||
handoff는 disk save나 integrity 완료를 관찰할 수 없으므로 계속
|
||||
`BROWSER_HANDOFF`다. 이 selector와 picker 미지원 failure normalization도 현재
|
||||
구현돼 있지 않다.
|
||||
|
||||
## 6. Image CDN
|
||||
|
||||
### 6.1 Asset와 preset
|
||||
|
||||
application은 raw source URL이나 arbitrary transform query 대신 opaque asset
|
||||
reference와 composition-registered preset reference를 전달한다. backend/CDN
|
||||
descriptor는 다음을 묶는다.
|
||||
|
||||
- opaque asset ID와 immutable asset revision
|
||||
- public/private delivery class
|
||||
- source pixel dimensions와 안전 판정을 통과한 raster media type
|
||||
- named preset와 exact crop/fit intent
|
||||
- rendition별 format, natural width/height, URL와 expiry
|
||||
- private rendition이면 server-issued capability binding
|
||||
|
||||
CDN은 임의 external source URL을 transform parameter로 받지 않는다. backend
|
||||
asset registry가 quarantine/scan을 통과한 source object만 CDN asset ID로
|
||||
promotion한다.
|
||||
|
||||
### 6.2 Policy validation
|
||||
|
||||
Composition policy가 소유하는 값:
|
||||
|
||||
- application origin과 그 origin과 다른 allowed HTTPS CDN origins/path prefix
|
||||
- named preset와 allowed widths/DPR/formats
|
||||
- max natural/output width·height·pixels와 decoded/encoded bytes
|
||||
- crop/fit, quality와 static-raster 제한
|
||||
- maximum candidates와 minimum private URL lifetime
|
||||
- maximum concurrent capability verification
|
||||
- public/private cache, referrer와 credential policy
|
||||
|
||||
runtime은 caller가 preset의 width, DPR, quality나 format ceiling을 늘리지 못하게
|
||||
한다. SVG/HTML/data/blob/javascript URL과 active/unknown media type은 기본
|
||||
거절한다. 현재 protocol은 static raster만 지원하므로 animated format은 항상
|
||||
거절하며, 도입하려면 별도 frame/decode budget protocol이 필요하다.
|
||||
Composition이 전달하는 hard limit은
|
||||
`IMAGE_CDN_IMPLEMENTATION_CEILINGS`보다 항상 작거나 같아야 한다. 이 값은 제품
|
||||
기본값이 아니라 adapter-owned 절대 상한이며 intrinsic/source/output pixel,
|
||||
decoded/encoded byte, candidate, URL, capability lifetime와 동시 cryptographic
|
||||
verification guard를 구성 실수로 해제하지 못하게 한다.
|
||||
|
||||
private capability의 signature는 versioned preset binding ID의 허용 집합을
|
||||
묶는다. CDN/BFF는 그 ID를 server-owned immutable preset registry에서 조회하고
|
||||
요청 query의 width/height/DPR/fit/format/quality가 registry가 산출한 exact
|
||||
candidate인지 다시 계산해 불일치 요청을 거절해야 한다. 브라우저가 만든 query,
|
||||
binding digest 또는 signature 문자열을 단순히 echo하거나 query 자체를
|
||||
authorization proof로 취급하지 않는다.
|
||||
|
||||
capability policy의 `acceptedKeyIds`는 bounded unique overlap set이며 현재
|
||||
signing key를 고르는 selector가 아니다. verifier의 immutable public-key
|
||||
registry는 runtime 생성 시 이 집합 전체를 포함해야 하고, descriptor의 단일
|
||||
`signature.keyId`는 policy와 verifier 양쪽에 exact membership이 있어야 한다.
|
||||
rotation은 새 public key와 old/new overlap policy 배포, client 채택 확인,
|
||||
backend signer 전환, `maxCapabilityLifetimeMs + maxClockSkewMs`와 client rollout
|
||||
기간 경과, old key 제거 순서를 따른다. 유출 key는 overlap 절차 대신 backend
|
||||
revocation과 runtime 재조합/강제 rollout 대상으로 다룬다.
|
||||
|
||||
browser probe는 encoded body를 hard cap 안에서 읽은 직후 native decoder 호출
|
||||
전에 PNG/JPEG/WebP/AVIF header/container metadata를 파싱한다. 선언된
|
||||
width/height, pixel 수와 decoded-byte ceiling을 먼저 확인하고 APNG/WebP
|
||||
animation, AVIF sequence/derived image와 ambiguous/malformed container를
|
||||
fail-closed한다. 이 pre-decode 검사가 통과한 static raster만
|
||||
`createImageBitmap`으로 실제 dimensions를 재검증한다.
|
||||
|
||||
### 6.3 Responsive descriptor
|
||||
|
||||
width descriptor를 쓰는 candidate는 모두 양의 고유 width를 가지며 오름차순으로
|
||||
정렬한다. 같은 source set에서 `w`와 `x` descriptor를 섞지 않는다. `sizes`는
|
||||
registry가 승인한 layout token에서 결정하고 arbitrary presentation 문자열을
|
||||
CDN query에 넣지 않는다.
|
||||
|
||||
반환값은 presentation-safe descriptor다.
|
||||
|
||||
- fallback `src`, intrinsic width/height
|
||||
- ordered format별 `srcset`
|
||||
- registry-owned `sizes`
|
||||
- `loading`, `decoding`, `fetchPriority`
|
||||
- `referrerPolicy=no-referrer`
|
||||
- application과 분리된 CDN origin의 asset은 `crossOrigin=anonymous`
|
||||
|
||||
private signed image는 expiry 전에 실제 load가 시작될 수 있는 eager/priority
|
||||
정책만 사용하거나 load 직전에 새 descriptor를 발급한다. 오래된 signed URL을
|
||||
DOM, persisted state, telemetry 또는 query cache에 장기 보관하지 않는다.
|
||||
private delivery는 `PRIMARY_REQUIRED` browser probe가 필수이며 이를 `NONE`으로
|
||||
낮출 수 없다. probe는 `credentials: omit`, redirect 금지, exact response URL과
|
||||
`Cache-Control: no-store`를 실제 response에서 확인한다.
|
||||
실제 `<img crossorigin="anonymous">`는 same-origin일 때 cookie를 보낼 수
|
||||
있으므로 registry는 CDN origin이 composition의 application origin과 같으면
|
||||
생성 단계에서 거절한다. private CDN 응답은 cookie나 ambient authorization에
|
||||
의존하지 않는다.
|
||||
|
||||
### 6.4 CDN cache와 invalidation
|
||||
|
||||
- public rendition은 asset revision을 URL에 포함하고
|
||||
`public, max-age=..., immutable`로 제공한다.
|
||||
- content가 바뀌면 purge에 의존해 같은 URL을 재사용하지 않고 revision을 바꾼다.
|
||||
- format은 URL에서 명시하거나 `Vary: Accept` 계약과 cache key를 정확히 맞춘다.
|
||||
- private rendition은 짧은 expiry와 필수 `no-store`를 쓴다.
|
||||
- CDN cache hit 여부는 authorization이나 asset safety proof가 아니다.
|
||||
|
||||
image probe의 단일 bounded deadline은 response header fetch, streamed body read와
|
||||
native decode 전체를 포함한다. timeout/cancel/error 시 reader를 cancel하고,
|
||||
abort 뒤 늦게 resolve한 response body도 cancel하며 늦게 생성된 `ImageBitmap`도
|
||||
즉시 `close()`한다.
|
||||
|
||||
Image CDN runtime의 `close()`는 terminal/idempotent다. application teardown,
|
||||
logout, account/tenant partition 변경 또는 runtime 교체 시 composition owner가
|
||||
한 번 호출한다. runtime lifetime signal은 진행 중 capability verification과
|
||||
probe를 중단하고, accepted capability WeakMap은 새 WeakMap으로 교체되어 기존
|
||||
reference를 즉시 revoke하면서 strong reference를 남기지 않는다. 닫힌 runtime은
|
||||
accept/resolve를 `UNAVAILABLE`로 거절하며 재개하지 않고 새 runtime을 조합한다.
|
||||
|
||||
### 6.5 아직 구현되지 않은 descriptor provider와 refresh
|
||||
|
||||
현재 Image CDN runtime은 trusted gateway가 이미 strict하게 decode했다고 가정한
|
||||
`BackendIssuedImageAsset` 또는 composition-owned public descriptor를
|
||||
`runtime.assets`에 전달받는다. opaque asset/preset validation, P-256 signature,
|
||||
responsive URL 생성과 browser probe는 구현돼 있지만 BFF에서 private descriptor를
|
||||
가져오는 concrete HTTP provider는 없다.
|
||||
|
||||
향후 `IMAGE_CDN_DESCRIPTOR_V1` provider는 다음 계약을 가진다.
|
||||
|
||||
- composition-owned fixed HTTPS BFF endpoint
|
||||
- caller가 전달하는 값은 opaque product asset reference와 registered intention뿐
|
||||
- authenticated control-plane request, redirect 금지와 no-store
|
||||
- exact final response URL/status/content type/content length
|
||||
- fatal UTF-8 bounded JSON body와 unknown-field rejection
|
||||
- issuer, asset/revision, dimensions, delivery class, preset binding ID,
|
||||
issued/expiry와 P-256 signature의 exact schema
|
||||
- request deadline, caller/runtime abort와 late response-body cleanup
|
||||
- descriptor와 raw backend body를 query cache, persistence, log와 telemetry에
|
||||
저장하지 않음
|
||||
|
||||
private descriptor가 minimum remaining lifetime 아래로 내려가면 presentation
|
||||
runtime이 기존 signed URL을 임의 연장하지 않는다. product-owned facade가 같은
|
||||
opaque asset/intention에 대해 single-flight reissue를 수행하고, 새 descriptor를
|
||||
다시 signature/registry/probe 경계에 통과시킨다. asset revision, preset binding,
|
||||
issuer 또는 account scope가 달라지면 기존 reference를 폐기하고 새 결과로
|
||||
교체한다. logout, tenant switch, key compromise와 kill switch에서는 refresh를
|
||||
중단하고 runtime을 `close()`한다.
|
||||
|
||||
P-256 key overlap과 runtime close는 현재 구현돼 있지만 dynamic key-set fetch,
|
||||
revocation epoch/list, descriptor auto-refresh와 backend scope revoke는 구현돼
|
||||
있지 않다. 정상 key rotation은 composition의 immutable old/new registry 교체로,
|
||||
긴급 회수는 backend revoke, runtime close와 forced rollout으로 처리한다.
|
||||
|
||||
presentation-safe descriptor를 실제 `<picture>/<source>/<img>`에 적용하는 renderer도
|
||||
현재 공통 runtime 범위에는 없다. renderer primitive는 raw URL override를 받지 않고
|
||||
descriptor 속성만 투영할 수 있지만, `alt`, placeholder, error/retry, SSR/preload와
|
||||
analytics는 제품 presentation이 소유한다.
|
||||
|
||||
## 7. Failure와 recovery
|
||||
|
||||
| 조건 | 결과 | 복구 |
|
||||
| --- | --- | --- |
|
||||
| capability expired/revoked | `EXPIRED_RESOURCE` | 새 capability 발급 |
|
||||
| method/origin/path/binding mismatch | `POLICY_REJECTED` | 요청 재구성 금지 |
|
||||
| part status conflict | `CONFLICT` | server reconcile |
|
||||
| part checksum mismatch | `INTEGRITY_FAILED` | 같은 bytes 재검증 후 retry/abort |
|
||||
| response overrun/truncation | `INTEGRITY_FAILED` | destination abort, 새 download |
|
||||
| session missing/gone | `EXPIRED_RESOURCE` | checkpoint 폐기 후 새 session |
|
||||
| image preset/URL/pixel violation | `POLICY_REJECTED` | 안전한 placeholder/original policy |
|
||||
| image capability verification concurrency ceiling | `LIMIT_EXCEEDED` | 진행 작업 종료 대기 또는 runtime 부하 조사 |
|
||||
| closed Image CDN runtime 사용 | `UNAVAILABLE` | 새 runtime composition |
|
||||
| network/429/모든 5xx | `UNAVAILABLE` | bounded retry/backoff |
|
||||
|
||||
관측성에는 operation, safe outcome, byte/part/candidate bucket, retry bucket과
|
||||
failure code만 기록한다. capability ID, URL, query, asset/resource/session ID,
|
||||
file name, digest, raw ETag, receipt와 backend message는 log/diagnostics/telemetry에
|
||||
기록하지 않는다. 앞 절의 strict checkpoint allowlist만 durable 예외다.
|
||||
|
||||
## 8. 조합 조건
|
||||
|
||||
현재 `src/adapters/browser-transfer/index.ts`는 개별 presigned, upload와 Image CDN
|
||||
factory를 export할 뿐 이들을 하나의 lifecycle과 account partition으로 묶는
|
||||
top-level composition factory를 제공하지 않는다. browser file runtime, capability
|
||||
vault, upload runtime과 Image CDN runtime은 각자 `dispose()`/`close()`를 가지지만
|
||||
logout, account switch와 partial cleanup을 하나의 admission fence 아래 실행하는
|
||||
owner도 아직 없다.
|
||||
|
||||
제품 composition 전에 반드시 정할 것:
|
||||
|
||||
- fixed BFF capability endpoint, closed upload endpoint map과 runtime schema
|
||||
- `PRESIGNED_TRANSFER_V1` 및 `IMAGE_CDN_DESCRIPTOR_V1` rollout/drain 계획
|
||||
- `PRESIGNED_MULTIPART_V1` canonical binding 재계산과 server-side session lookup
|
||||
- same-origin proxy 또는 cross-origin CORS/object-storage topology
|
||||
- per-account/partition Web Lock namespace와 ephemeral BroadcastChannel cancel
|
||||
namespace, 미지원 환경의 bounded abort fallback
|
||||
- per-operation maximum bytes, part size/count/concurrency와 retry budget
|
||||
- upload success status/receipt header/`expectedResponseByteLength`와 response cap
|
||||
- checksum algorithm과 full/composite 의미
|
||||
- quarantine scan/promotion/status protocol
|
||||
- checkpoint classification, account scope, retention과 logout handling
|
||||
- application과 분리된 CDN origin, versioned preset exact 재계산,
|
||||
format/pixel/decoded-byte/cache/CSP contract, key rotation,
|
||||
verification concurrency와 runtime/probe deadline
|
||||
- download save/handoff UX와 partial destination recovery
|
||||
- browser matrix, fault injection, orphan cleanup과 CDN rollback runbook
|
||||
|
||||
이 값이 없으면 runtime factory를 bootstrap에 넣지 않는다. 선택하지 않은 runtime은
|
||||
production module inventory와 removal gate로 기본 bundle에서 제외한다.
|
||||
|
||||
### 8.1 목표 top-level composition
|
||||
|
||||
향후 공통 factory의 책임은 dependency를 편리하게 묶는 것보다 구성 시 불변조건과
|
||||
teardown 순서를 한 곳에서 강제하는 것이다.
|
||||
|
||||
```text
|
||||
BrowserTransferComposition
|
||||
product facade references
|
||||
strict runtime config snapshot
|
||||
browser file runtime
|
||||
browser-managed capability provider/vault
|
||||
presigned provider/vault/executor
|
||||
upload control transport/runtime/checkpoint admin
|
||||
download strategy selector
|
||||
optional Range runtime/checkpoint/destination registry
|
||||
image descriptor provider/runtime
|
||||
account lifecycle fence
|
||||
readiness/compatibility result
|
||||
safe observer
|
||||
kill switches
|
||||
close()
|
||||
```
|
||||
|
||||
browser-managed handoff mechanism에는 synchronous resolver seam이 있지만, 실제
|
||||
BFF에서 capability를 발급받아 user activation 전에 in-memory identity vault에
|
||||
준비하는 concrete provider는 현재 없다. 목표 provider는 fixed endpoint, strict
|
||||
versioned response, safe receipt와 exact resource/media/extension/length/digest/expiry
|
||||
binding을 검증하고 raw href는 resolver 내부에만 둔다. handoff 시점에는 async
|
||||
발급을 시작하지 않고 이미 준비된 exact identity만 synchronous consume한다.
|
||||
|
||||
composition의 `close()`는 terminal/idempotent하고 다음 순서를 보장한다.
|
||||
|
||||
1. 신규 issue/upload/download/image resolve admission을 닫는다.
|
||||
2. active foreground work와 worker/channel을 abort한다.
|
||||
3. in-memory signed capability와 image reference를 revoke한다.
|
||||
4. writer, reader, Web Lock, BroadcastChannel과 runtime을 close한다.
|
||||
5. upload/Range checkpoint와 owned staging을 정책에 따라 reconcile한다.
|
||||
6. logout/account deletion이면 maintenance authority로 exact partition cleanup을
|
||||
실행한다.
|
||||
7. blocked/ambiguous cleanup을 성공으로 표시하지 않고 safe recovery 결과로 남긴다.
|
||||
|
||||
primary status가 `COMPOSED`여도 completion ledger의 `RuntimeHealth`,
|
||||
`PromotionEvidence`와 `TrafficAdmission`은 별도다. config schema, endpoint/auth,
|
||||
account partition, actual provider evidence, cleanup owner와 browser fallback 중
|
||||
하나라도 없으면 `TrafficAdmission=DISABLED`,
|
||||
`PromotionEvidence=MISSING | PARTIAL | EXPIRED`로 fail-closed한다.
|
||||
|
||||
### 8.2 Contract harness 목표
|
||||
|
||||
현재 unit test와 Playwright route interception은 reference runtime의 failure와
|
||||
browser API behavior를 검증하지만 reusable BFF/provider conformance suite는 아니다.
|
||||
`server-file-capability-infrastructure.md`의 provider contract matrix도 설계이며 실제
|
||||
S3/MinIO/GCS/Azure/CDN adapter에 실행되는 source는 이 repository에 없다.
|
||||
|
||||
향후 harness는 같은 versioned fixture를 다음 네 등급에 실행한다.
|
||||
|
||||
| 등급 | 증명 범위 |
|
||||
| --- | --- |
|
||||
| deterministic fake | state machine, canonical binding과 failure mapping |
|
||||
| intercepted browser route | Fetch/CORS/stream/abort와 native destination |
|
||||
| emulator/container | provider SDK, multipart와 response header wiring |
|
||||
| actual staging provider | 실제 product/API/version/region의 constraint 강제 |
|
||||
|
||||
필수 frontend/BFF contract:
|
||||
|
||||
- `PRESIGNED_TRANSFER_V1` exact request/response와 unknown version rejection
|
||||
- expiry, wrong method/resource/range/part/header/query/origin rejection
|
||||
- URL/query/header/error/telemetry redaction
|
||||
- capability reissue와 server revocation
|
||||
- upload create/status/part/complete/abort, response-loss와 orphan reconcile
|
||||
- pause/checkpoint inventory가 구현될 경우 v1→v2 migration과 old-writer drain
|
||||
- browser-managed capability preload/consume와 `BROWSER_HANDOFF` truth
|
||||
- `IMAGE_CDN_DESCRIPTOR_V1`, signature/key overlap, expiry/reissue와 revocation
|
||||
- CDN preset exact recomputation, private no-store와 public immutable cache
|
||||
- Range를 구현할 경우 VD-14의 `200/206/412/416` 및 destination crash matrix
|
||||
|
||||
actual provider evidence에는 adapter/provider/config/contract version, artifact
|
||||
digest, environment/region, pass/fail/skip, fault result, waiver/owner와 expiry를
|
||||
기록한다. required case skip, expired evidence와 contract/config 변경은 production
|
||||
promotion을 막는다. fake나 emulator success를 actual provider evidence로
|
||||
승격하지 않는다.
|
||||
|
||||
## 9. 표준·vendor 참고
|
||||
|
||||
- [Browser data capability completion ledger](./browser-data-capability-completion-ledger.md)
|
||||
- [Fetch Standard](https://fetch.spec.whatwg.org/)
|
||||
- [RFC 9110 HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110.html)
|
||||
- [File System Standard](https://fs.spec.whatwg.org/)
|
||||
- [VD-14 resumable download와 background download](./decisions/VD-14-resumable-download-and-background-transfer.md)
|
||||
- [VD-16 browser transfer composition과 image delivery](./decisions/VD-16-browser-transfer-composition-and-image-delivery.md)
|
||||
- [HTML responsive images](https://html.spec.whatwg.org/multipage/images.html)
|
||||
- [tus resumable upload protocol](https://tus.io/protocols/resumable-upload)
|
||||
- [Amazon S3 presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html)
|
||||
- [Amazon S3 multipart upload](https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html)
|
||||
@@ -0,0 +1,775 @@
|
||||
# Protobuf browser transport와 REST Gateway
|
||||
|
||||
- 상태: production design accepted, product/provider selection pending
|
||||
- 기준일: 2026-07-28
|
||||
- 범위: gRPC-Web, Connect-Web, Connect protocol, Protobuf contract/codegen,
|
||||
REST/JSON Gateway
|
||||
- 현재 source 상태: provider-neutral Browser RPC V3 계약·application port·공통
|
||||
unary/server-stream lifecycle runtime은 `AVAILABLE_NOT_COMPOSED`;
|
||||
vendor runtime/dependency/proto/descriptor/proxy는 없음
|
||||
- 관련 결정:
|
||||
- [VD-23 API transport selection과 REST execution](./decisions/VD-23-api-transport-selection-and-rest-execution.md)
|
||||
- [VD-24 Runtime schema와 boundary mapper](./decisions/VD-24-runtime-schema-and-boundary-mapper.md)
|
||||
- [VD-25 Server state cache lifecycle](./decisions/VD-25-server-state-cache-lifecycle.md)
|
||||
- [VD-27 gRPC-Web unary와 server stream](./decisions/VD-27-grpc-web-unary-and-server-stream.md)
|
||||
- [VD-29 Connect-Web와 browser Protobuf runtime](./decisions/VD-29-connect-web-and-browser-protobuf-runtime.md)
|
||||
- [VD-30 Protobuf contract와 REST Gateway](./decisions/VD-30-protobuf-contract-and-rest-gateway.md)
|
||||
- backend handoff:
|
||||
[Backend API와 Server State contract](./backend-api-and-server-state-contract.md)
|
||||
- 운영 절차:
|
||||
[API contract와 server-state recovery](../operations/api-contract-and-server-state-recovery.md)
|
||||
|
||||
## 1. 먼저 축을 분리한다
|
||||
|
||||
네 이름은 같은 종류의 대안이 아니다.
|
||||
|
||||
| 축 | 선택지 | 소유하는 것 |
|
||||
| --- | --- | --- |
|
||||
| contract/serialization | Protobuf binary, ProtoJSON | message/service schema, field number, presence, codegen |
|
||||
| browser RPC transport | Connect protocol, gRPC-Web | HTTP framing, media, status/error, timeout, stream terminal |
|
||||
| browser client runtime | Connect-Web, official grpc-web 또는 승인 runtime | Fetch/XHR, generated client, interceptors, cancellation |
|
||||
| HTTP exposure | curated REST BFF, generated gRPC-Gateway, Envoy JSON transcoder | resource URL, method, JSON/status/cache/CORS contract |
|
||||
|
||||
예를 들어 Connect-Web은 같은 generated service descriptor로 Connect protocol과
|
||||
gRPC-Web transport를 모두 만들 수 있다. 반대로 REST Gateway는 Protobuf service를
|
||||
ProtoJSON HTTP API로 노출할 수 있지만, 그것이 자동으로 좋은 browser REST
|
||||
contract가 된다는 뜻은 아니다.
|
||||
|
||||
한 semantic operation은 frontend registry에서 정확히 하나의 active wire
|
||||
profile에 binding한다. runtime이 media type을 보고 Connect/gRPC-Web/REST를
|
||||
추측하거나 장애 시 다른 protocol로 같은 command를 자동 replay하지 않는다.
|
||||
|
||||
## 2. 현재 상태
|
||||
|
||||
| capability | current status | 근거 | promotion 필요 |
|
||||
| --- | --- | --- | --- |
|
||||
| Browser RPC V3 공통 계약/runtime | `AVAILABLE_NOT_COMPOSED` | exact operation/profile/schema/mapper/encoder/transport join, unary retry·total deadline·abort, bounded server-stream·idle deadline·terminal, generation fence와 unavailable adapter test | selected generated client를 감싸는 protocol transport, raw-byte cap과 actual provider/browser conformance |
|
||||
| Protobuf schema/codegen | `DESIGNED_NOT_IMPLEMENTED` | `.proto`, `buf.yaml`, descriptor, runtime dependency 없음 | authenticated schema source와 owner |
|
||||
| Connect-Web client runtime | `DESIGNED_NOT_IMPLEMENTED` | `@connectrpc/*`, `@bufbuild/protobuf` 없음 | selected service, provider와 bundle budget |
|
||||
| Connect protocol unary | `DESIGNED_NOT_IMPLEMENTED` | endpoint/profile/fixture 없음 | exact JSON 또는 binary profile |
|
||||
| Connect protocol server stream | `DESIGNED_NOT_IMPLEMENTED` | stream runtime/proxy evidence 없음 | bounded stream protocol과 browser evidence |
|
||||
| gRPC-Web unary/server stream | `DESIGNED_NOT_IMPLEMENTED` | VD-27만 존재 | runtime/proxy/descriptor와 conformance |
|
||||
| REST Gateway reference contract/harness | `DESIGNED_NOT_IMPLEMENTED` | native REST reference만 있고 transcoder/fixture 없음 | selected kind의 deterministic/provider conformance |
|
||||
| product REST Gateway composition | `NOT_SELECTED` | route/provider/owner 없음 | curated BFF 또는 selected transcoder의 제품 승인 |
|
||||
| browser client/bidi stream | `PLATFORM_LIMITED` | request streaming을 target browser 공통 계약으로 보장 못함 | 다른 transport/application protocol |
|
||||
| product traffic | `NOT_SELECTED` | operation/provider/owner 없음 | product ADR와 traffic admission |
|
||||
|
||||
이 문서가 추가돼도 dependency나 generated source를 기본 bundle에 넣지 않는다.
|
||||
공통 runtime source가 생긴 뒤에도 같은 원칙을 유지한다. 공통 runtime은
|
||||
Connect/gRPC-Web wire를 직접 decode하지 않고 selected transport가 반환한
|
||||
bounded message/terminal/failure만 처리하므로, 실제 Connect-Web 또는 gRPC-Web
|
||||
adapter가 구현됐다는 증거가 아니다.
|
||||
|
||||
## 3. 기본 선택
|
||||
|
||||
### 3.1 권장 순서
|
||||
|
||||
1. 기존 REST가 제품 의미와 운영 요구를 충족하면 REST를 유지한다.
|
||||
2. backend가 Protobuf-first이고 browser RPC가 필요하면 Connect-Web +
|
||||
Connect protocol을 우선 평가한다.
|
||||
3. backend가 gRPC-Web만 노출하거나 기존 Envoy/gRPC-Web conformance 자산을
|
||||
재사용해야 하면 Connect-Web의 gRPC-Web transport 또는 official grpc-web
|
||||
runtime 중 하나를 고정한다.
|
||||
4. public HTTP API, CDN/conditional cache, 링크 가능한 resource URL,
|
||||
broad HTTP tooling이 핵심이면 curated REST BFF를 우선한다.
|
||||
5. `google.api.http` annotation만으로 제품 REST 의미를 온전히 표현할 수 있을
|
||||
때만 generated gRPC-Gateway/Envoy transcoding을 선택한다.
|
||||
|
||||
### 3.2 선택 matrix
|
||||
|
||||
| 요구 | 기본 후보 | 주의 |
|
||||
| --- | --- | --- |
|
||||
| 내부 web UI + Protobuf-first backend + unary | Connect-Web/Connect | JSON/binary를 operation profile로 고정 |
|
||||
| 내부 web UI + 기존 gRPC-Web gateway | selected gRPC-Web runtime | runtime별 streaming capability가 다름 |
|
||||
| bounded server→browser stream | Connect server stream 또는 gRPC-Web server stream | idle/total/queue/sequence/resume 필수 |
|
||||
| public/resource-oriented HTTP API | curated REST BFF | Protobuf service shape를 그대로 노출하지 않음 |
|
||||
| 단순 proto HTTP annotation과 broad JSON client | generated REST Gateway | ProtoJSON/status/error semantic fixture 필요 |
|
||||
| HTTP cache/ETag/Range/file download | REST/BFF | RPC transcoder로 억지로 만들지 않음 |
|
||||
| browser client/bidi | WebSocket/WebTransport/별도 session API | Connect protocol 자체 지원과 browser 지원을 혼동 금지 |
|
||||
| 기존 REST backend뿐임 | REST 유지 | Protobuf/gateway를 미래 대비로 추가하지 않음 |
|
||||
|
||||
Connect가 항상 gRPC-Web보다 우월하거나 REST Gateway가 수동 REST보다 항상
|
||||
저렴하다고 가정하지 않는다. actual provider, proxy, browser, bundle,
|
||||
observability와 조직 운영 비용을 evidence로 비교한다.
|
||||
|
||||
## 4. 공통 application 경계
|
||||
|
||||
```text
|
||||
presentation
|
||||
-> feature application input
|
||||
-> semantic gateway port
|
||||
-> installed operation registry
|
||||
-> REST adapter
|
||||
-> Connect adapter
|
||||
-> gRPC-Web adapter
|
||||
-> transport decode
|
||||
-> semantic runtime schema
|
||||
-> boundary mapper
|
||||
-> immutable application projection
|
||||
```
|
||||
|
||||
금지:
|
||||
|
||||
```text
|
||||
page/use-case -> generated service client
|
||||
page/use-case -> protobuf message or descriptor
|
||||
page/use-case -> transport/base URL/metadata
|
||||
generated message -> TanStack cache
|
||||
raw Connect/gRPC error -> presentation
|
||||
REST gateway DTO -> domain without runtime schema/mapper
|
||||
```
|
||||
|
||||
application port는 `listResources`, `createResource`, `watchJob` 같은 의미를
|
||||
표현한다. `callRpc(service, method, bytes)`나 `executeProto<T>()`를 노출하지 않는다.
|
||||
|
||||
## 5. Protocol-neutral operation과 exact binding
|
||||
|
||||
```text
|
||||
ApiOperationContractV3
|
||||
semanticOperationId
|
||||
owner
|
||||
semantics = QUERY | COMMAND | SERVER_STREAM
|
||||
protocol = REST | CONNECT_HTTP | GRPC_WEB
|
||||
replayPolicy
|
||||
idempotencyKeyPolicy
|
||||
authProfileId
|
||||
csrfProfileId
|
||||
deadlineProfileId
|
||||
retryProfileId
|
||||
requestSemanticSchemaId
|
||||
responseSemanticSchemaId
|
||||
mapperId
|
||||
serverStateProfileId | null
|
||||
dataClassification
|
||||
compatibility
|
||||
protocolBinding
|
||||
```
|
||||
|
||||
현재 source에서는 설치된 REST V2 registry를 위험한 union migration으로 바꾸지
|
||||
않고 `BrowserRpcOperationV3` sibling registry를 추가했다. 제품 operation이
|
||||
선택되면 composition root가 semantic feature gateway 뒤에서 REST 또는 Browser
|
||||
RPC 중 정확히 하나를 bind한다. use-case가 두 registry를 보거나 runtime fallback을
|
||||
결정하지 않는다.
|
||||
|
||||
Connect/gRPC-Web binding:
|
||||
|
||||
```text
|
||||
BrowserRpcOperationV3 + BrowserRpcProviderProfile
|
||||
providerId
|
||||
clientRuntimeId
|
||||
clientRuntimeVersion
|
||||
transportRuntimeKind = CONNECT_WEB_FETCH | OFFICIAL_GRPC_WEB_XHR | CUSTOM_FETCH_FRAMED
|
||||
clientApiKind = PROMISE | ASYNC_ITERABLE | CALLBACK_STREAM
|
||||
protocolRevision
|
||||
wireProfileId
|
||||
encoding = PROTO_BINARY | PROTO_JSON
|
||||
fullyQualifiedService
|
||||
method
|
||||
rpcKind = UNARY | SERVER_STREAM
|
||||
requestMessageId
|
||||
responseMessageId
|
||||
descriptorArtifactId
|
||||
descriptorDigest
|
||||
errorProfileId
|
||||
corsProfileId
|
||||
compressionProfileId
|
||||
deadlineDialect
|
||||
retryOwner = FRONTEND_ADAPTER | EDGE_PROXY | NONE
|
||||
maxRequestBytes
|
||||
maxHeaderBytes
|
||||
maxHeaderFields
|
||||
maxMessageBytes
|
||||
maxResponseMessages
|
||||
maxTotalResponseBytes
|
||||
idleDeadlineMs | null
|
||||
```
|
||||
|
||||
구현 경로는 `src/contracts/browser-rpc.ts`,
|
||||
`src/application/ports/browser-rpc`, `src/adapters/browser-rpc`다. 공통 profile은
|
||||
runtime identity/digest, fixed base URL, descriptor digest, allowed procedure,
|
||||
auth/CSRF/CORS/error/deadline/retry owner와 raw-byte ceiling owner까지 고정한다.
|
||||
`maxHeaderBytes`, media/trailer parsing과 실제 raw queue ceiling은 공통 coordinator가
|
||||
추측하지 않고 selected wire transport가 VD-27/VD-29에 따라 추가로 집행한다.
|
||||
|
||||
REST Gateway binding:
|
||||
|
||||
```text
|
||||
RestGatewayBindingV1
|
||||
providerId
|
||||
gatewayKind = CURATED_BFF | GRPC_GATEWAY | ENVOY_TRANSCODER
|
||||
gatewayRuntimeId
|
||||
gatewayRuntimeVersion
|
||||
httpRuleArtifactId
|
||||
httpRuleDigest
|
||||
method
|
||||
relativePathTemplate
|
||||
requestProjection
|
||||
protoJsonProfileId | null
|
||||
responseEnvelopeProfileId
|
||||
statusErrorProfileId
|
||||
cacheConditionalProfileId
|
||||
```
|
||||
|
||||
registry는 다음을 boot/build 전에 거절한다.
|
||||
|
||||
- protocol/profile/runtime/provider tuple 불일치
|
||||
- descriptor 또는 HTTP rule digest 불일치
|
||||
- caller-provided endpoint, method, metadata 또는 message type
|
||||
- `SERVER_STREAM`인데 ordinary Query profile 사용
|
||||
- browser client/bidi method
|
||||
- Connect와 gRPC-Web decoder의 runtime auto-negotiation
|
||||
- runtime kind와 deadline/cancel/status API가 맞지 않는 client API 조합
|
||||
- frontend와 edge proxy가 동시에 retry owner인 profile
|
||||
- gateway kind가 다른 두 route의 last-write-wins collision
|
||||
- JSON exposure인데 binary-only compatibility gate만 통과
|
||||
- non-replayable command retry/fallback
|
||||
- hard ceiling보다 큰 message/frame/deadline/URL
|
||||
|
||||
## 6. Protobuf contract source
|
||||
|
||||
### 6.1 Source of truth
|
||||
|
||||
```text
|
||||
authenticated Proto/Buf module
|
||||
-> immutable source commit/module digest
|
||||
-> lint
|
||||
-> breaking comparison
|
||||
-> FileDescriptorSet
|
||||
-> deterministic codegen
|
||||
-> generated artifact digest
|
||||
-> operation/schema/mapper binding
|
||||
-> release contract set
|
||||
```
|
||||
|
||||
runtime endpoint에서 latest schema를 받아 build하지 않는다. schema update는
|
||||
review 가능한 explicit workflow이며 source, dependency lock, descriptor,
|
||||
plugin/runtime와 generated output digest를 함께 보존한다.
|
||||
|
||||
### 6.2 Evolution
|
||||
|
||||
- field number 재사용/renumber 금지
|
||||
- 삭제 field number와 JSON name reserve
|
||||
- package/service/method full name 안정성
|
||||
- enum zero value와 unknown numeric policy
|
||||
- proto3 optional/edition presence를 명시
|
||||
- oneof absence/unknown case 처리
|
||||
- map ordering을 identity/digest로 사용하지 않음
|
||||
- unknown field가 binary↔JSON conversion에서 보존되지 않을 수 있음을 반영
|
||||
- Timestamp/Duration range와 nanos
|
||||
- int64/uint64의 JS/ProtoJSON projection
|
||||
- bytes와 repeated/nesting ceiling
|
||||
|
||||
JSON을 한 곳이라도 사용하면 binary wire compatibility만으로 충분하지 않다.
|
||||
최소 `WIRE_JSON` breaking category를 요구한다. generated SDK의 import/source
|
||||
compatibility를 외부 소비자가 의존하면 `PACKAGE` 또는 `FILE`을 선택한다.
|
||||
Buf gate와 별도로 domain meaning, authorization, default/presence, pagination,
|
||||
revision과 idempotency semantic diff를 수행한다.
|
||||
|
||||
### 6.3 ProtoJSON profile
|
||||
|
||||
operation은 다음을 고정한다.
|
||||
|
||||
```text
|
||||
ProtoJsonProfileV1
|
||||
emitDefaultValues
|
||||
useProtoFieldNames
|
||||
enumEncoding = NAME | NUMBER
|
||||
ignoreUnknownFields
|
||||
int64Projection
|
||||
bytesProjection
|
||||
wellKnownTypePolicy
|
||||
```
|
||||
|
||||
runtime/library default에 맡기지 않는다. 기본 방향:
|
||||
|
||||
- request unknown field reject
|
||||
- response는 generated decoder 뒤 known semantic projection만 mapper에 전달
|
||||
- int64/uint64는 decimal string 또는 bounded application type
|
||||
- bytes는 base64 decode 전후 byte ceiling
|
||||
- non-finite float는 domain 승인 없으면 거절
|
||||
- Timestamp/Duration은 generated decode 성공 뒤 semantic range 재검증
|
||||
- `Any`는 allowlisted type URL만
|
||||
|
||||
ProtoJSON은 ordinary JSON schema의 임의 union을 대체하지 않으며 binary보다
|
||||
evolution 보장이 약하다.
|
||||
|
||||
## 7. Deterministic codegen과 공급망
|
||||
|
||||
선택 branch는 최소 다음을 고정한다.
|
||||
|
||||
```text
|
||||
buf.yaml
|
||||
buf.lock
|
||||
buf.gen.yaml
|
||||
schema/module digest
|
||||
protoc or Buf version
|
||||
plugin name/version/revision/digest
|
||||
@bufbuild/protobuf version
|
||||
@connectrpc/connect version
|
||||
@connectrpc/connect-web version
|
||||
Buf image digest
|
||||
FileDescriptorSet digest
|
||||
canonical HttpRule manifest digest
|
||||
generated OpenAPI digest when selected
|
||||
generated output digest
|
||||
```
|
||||
|
||||
CI:
|
||||
|
||||
- format/lint/breaking
|
||||
- dependency lock과 source provenance
|
||||
- clean checkout generate diff 0
|
||||
- generated directory 수동 수정 금지
|
||||
- generated import boundary
|
||||
- descriptor↔generated symbol exact join
|
||||
- generated runtime↔runtime compatibility matrix
|
||||
- canonical HttpRule/OpenAPI semantic diff; custom option을 Buf breaking에 위임하지 않음
|
||||
- N/N-1 fixture
|
||||
- license/SBOM/vulnerability/secret scan
|
||||
- production bundle inventory와 budget
|
||||
- capability removal 뒤 generated/runtime/proxy reference 0
|
||||
|
||||
generated files는 `src/generated` 같은 전역 public API가 아니라 selected
|
||||
feature adapter-private root에 둔다. 여러 feature가 공유하는 service라도 좁은
|
||||
platform adapter facade만 재사용한다.
|
||||
|
||||
## 8. Connect-Web
|
||||
|
||||
Connect-Web은 browser client runtime이다. `createConnectTransport()`는 Connect
|
||||
protocol, `createGrpcWebTransport()`는 gRPC-Web protocol을 사용한다. 같은 package를
|
||||
쓴다고 두 wire protocol이 호환되거나 자동 failover 가능한 것은 아니다.
|
||||
Connect-ES v2 generation은 `protoc-gen-es`가 message와 service descriptor를
|
||||
함께 생성하며 과거 Connect 전용 generator를 신규 toolchain에 넣지 않는다.
|
||||
|
||||
### 8.1 Connect unary
|
||||
|
||||
```text
|
||||
validated semantic input
|
||||
-> generated request
|
||||
-> bounded encode (ProtoJSON or binary)
|
||||
-> POST fixed /package.Service/Method
|
||||
-> bounded response/error decode
|
||||
-> generated message
|
||||
-> semantic validation
|
||||
-> mapper
|
||||
-> generation fence
|
||||
```
|
||||
|
||||
Connect unary는 bare Protobuf/JSON body와 의미 있는 HTTP status를 사용한다.
|
||||
success content type과 selected encoding을 exact 검증한다. error는 non-2xx JSON
|
||||
Connect error profile로만 decode하며 intermediary HTML/JSON을 Connect error로
|
||||
추측하지 않는다.
|
||||
|
||||
stock runtime이 unary body를 whole JSON/ArrayBuffer로 읽는 selected version이면
|
||||
interceptor가 raw-byte ceiling을 대신한다고 기록하지 않는다. edge의 encoded/
|
||||
decompressed cap과 platform-owned bounded Fetch transport 또는 해당 exact
|
||||
runtime의 overflow conformance가 있어야 production evidence가 닫힌다.
|
||||
|
||||
GET은 다음을 모두 만족할 때만 별도 profile로 허용한다.
|
||||
|
||||
- protobuf method `idempotency_level = NO_SIDE_EFFECTS`
|
||||
- public/non-sensitive bounded request
|
||||
- URL byte ceiling과 canonical encoding
|
||||
- header/credential/preflight 정책
|
||||
- exact CDN/browser cache key, `Vary`, ETag와 retention
|
||||
- request URL이 log/history/referrer에 노출돼도 허용되는 classification
|
||||
|
||||
인증/private query의 기본은 POST다.
|
||||
|
||||
### 8.2 Connect server stream
|
||||
|
||||
Connect streaming은 framed `application/connect+proto|json`이고 HTTP status
|
||||
`200` 안의 final EndStream envelope가 RPC error/trailer authority다.
|
||||
|
||||
decoder는:
|
||||
|
||||
- incremental 5-byte envelope prefix
|
||||
- flag/compression/declared length
|
||||
- per-message/decompressed/total/count ceiling
|
||||
- exactly one final EndStream envelope
|
||||
- data after EndStream, missing EndStream와 truncation
|
||||
- bounded error/detail/trailer projection
|
||||
- idle + total deadline
|
||||
- reader cancel/release와 bounded consumer queue
|
||||
|
||||
를 검증한다. network EOF는 success가 아니다. stock Connect-Web runtime을
|
||||
사용하면 exact package version이 이 state machine을 얼마나 집행하는지
|
||||
conformance로 증명하고, 부족한 hard ceiling은 proxy 또는 custom Transport가
|
||||
소유한다. selected stock runtime의 streaming output compression은
|
||||
`IDENTITY_ONLY`로 고정하고 지원하지 않는 compressed envelope를 광고하지 않는다.
|
||||
|
||||
### 8.3 Interceptor policy
|
||||
|
||||
interceptor가 허용되는 책임:
|
||||
|
||||
- registry-owned auth/CSRF patch
|
||||
- remaining deadline/timeout
|
||||
- fixed low-cardinality diagnostics
|
||||
- exact safe retry coordinator
|
||||
- safe error normalization
|
||||
|
||||
금지:
|
||||
|
||||
- arbitrary URL/service/method rewrite
|
||||
- raw message/error logging
|
||||
- caller metadata passthrough
|
||||
- operation semantic retry를 transport가 임의 결정
|
||||
- cache write와 domain mapping
|
||||
|
||||
transport/client는 runtime/scope 단위로 재사용하되 base URL/profile이 다른
|
||||
provider 사이에서 공유하지 않는다.
|
||||
|
||||
interceptor array의 선언 순서가 아니라 실제 onion execution order를 manifest와
|
||||
test에 보존한다. remaining total deadline이 0 이하면 `timeoutMs=0`을 넘기지 않고
|
||||
호출 전에 local deadline failure로 닫는다.
|
||||
|
||||
## 9. gRPC-Web
|
||||
|
||||
상세 frame/status state machine은 VD-27을 따른다.
|
||||
|
||||
- browser는 native gRPC/HTTP2 transport를 직접 사용한다고 가정하지 않는다.
|
||||
- official `grpc-web` runtime은 XHR와 runtime-owned frame/status decoder,
|
||||
`ClientReadableStream.cancel()`을 사용한다. raw `ReadableStream` frame parser와
|
||||
`AbortSignal`을 이 경로의 frontend 보장으로 기록하지 않는다.
|
||||
- custom Fetch runtime만 `fetch`/`AbortController`/incremental raw-frame decoder
|
||||
profile을 사용한다. 두 runtime은 `transportRuntimeKind`로 분리한다.
|
||||
- gRPC-Web response trailer는 body trailer frame 또는 trailers-only response
|
||||
header에서 해석한다.
|
||||
- `grpc-status`가 있으면 그것이 authoritative다. 없으면 native gRPC의 공식
|
||||
HTTP→gRPC fallback mapping으로 internal status를 만들며 HTTP success만으로
|
||||
RPC success를 판정하지 않는다.
|
||||
- official `grpc-web` JavaScript runtime은 binary unary와 text unary/server
|
||||
streaming capability를 구분한다.
|
||||
- Connect-Web gRPC-Web transport를 선택하면 별도 `clientRuntimeId`와 그 runtime의
|
||||
conformance matrix를 사용한다.
|
||||
- text/base64와 binary를 같은 byte budget으로 취급하지 않는다.
|
||||
- text decoder는 browser chunk 하나를 base64 entity 하나로 가정하지 않고,
|
||||
중간 padding이 있는 연속 base64 entity를 처리해야 한다.
|
||||
- Envoy `grpc_web` filter 또는 selected gateway의 exact version/config를
|
||||
provider profile에 binding한다.
|
||||
- Envoy profile은 filter order, upstream HTTP/2, route/idle/max-stream timeout,
|
||||
gRPC timeout offset, buffering/flush, header/message ceiling과 local reply
|
||||
mapping을 고정한다. server stream에는 default route timeout을 그대로 쓰지 않는다.
|
||||
- retry owner는 정확히 하나다. server stream replay/hedge는 금지하고, frontend와
|
||||
Envoy retry를 동시에 켜지 않는다.
|
||||
|
||||
gRPC-Web과 Connect stream은 terminal envelope가 서로 다르다. 공통
|
||||
`ReadableStream` helper를 재사용할 수 있어도 decoder/state machine을 합치지 않는다.
|
||||
|
||||
## 10. REST Gateway
|
||||
|
||||
### 10.1 세 종류를 분리한다
|
||||
|
||||
`CURATED_BFF`
|
||||
|
||||
- browser/resource contract를 별도로 설계
|
||||
- REST envelope, status, ETag/304/412, idempotency, pagination과 CORS를 직접 소유
|
||||
- 내부에서 gRPC/Connect service를 호출할 수 있으나 public DTO는 별도 mapper
|
||||
|
||||
`GRPC_GATEWAY`
|
||||
|
||||
- `google.api.http` annotation에서 reverse proxy와 optional OpenAPI를 생성
|
||||
- proto request field를 path/query/body로 projection
|
||||
- ProtoJSON과 gRPC status mapping을 exact profile로 고정
|
||||
- initial reference 후보는 unary만이며 REST streaming은 별도 ADR 없이는 선택하지 않음
|
||||
|
||||
`ENVOY_TRANSCODER`
|
||||
|
||||
- descriptor와 `google.api.http` annotation으로 proxy filter가 JSON↔gRPC 변환
|
||||
- filter/runtime config와 descriptor를 coherent artifact로 배포
|
||||
- application-specific envelope/cache/idempotency 의미는 별도 filter/BFF 없이는
|
||||
자동 생성되지 않음
|
||||
|
||||
한 route는 하나의 gateway kind만 소유한다.
|
||||
|
||||
현재 reference REST의 `{ success, data|error, meta }` envelope와
|
||||
`200|201`, 향후 `204/304/412` 의미는 direct generated gateway의 기본 출력과
|
||||
같지 않다. 따라서 기존 reference operation은 curated BFF를 유지한다. direct
|
||||
gateway는 ProtoJSON/HTTP rule/status/error 자체를 새 versioned public contract로
|
||||
승인한 operation에만 적용한다.
|
||||
|
||||
### 10.2 Automatic transcoding의 한계
|
||||
|
||||
HTTP annotation은 path/method/body projection을 정의하지만 다음을 자동으로
|
||||
완성하지 않는다.
|
||||
|
||||
- frontend의 strict success/failure envelope
|
||||
- product authorization와 existence hiding
|
||||
- idempotency store/effect certainty
|
||||
- CursorPage snapshot 의미
|
||||
- ETag/If-None-Match/If-Match와 revision CAS
|
||||
- CDN/private cache policy
|
||||
- domain error vocabulary와 validation detail redaction
|
||||
- file Range/streaming download/upload
|
||||
- browser-compatible server event/reconnect protocol
|
||||
- OpenAPI와 runtime response/error rewrite의 자동 coherence
|
||||
|
||||
따라서 현재 reference REST envelope에 generated gateway를 바로 연결할 수 없다.
|
||||
gateway adapter가 exact envelope/status를 제공하거나 frontend에 새 operation
|
||||
contract를 versioned로 추가해야 한다.
|
||||
|
||||
### 10.3 REST mapping
|
||||
|
||||
- resource name/path는 stable API meaning을 가져야 한다.
|
||||
- path field를 body/query에도 중복 projection하지 않는다.
|
||||
- unbound fields의 query mapping과 repeated/nested encoding을 fixture로 고정한다.
|
||||
- `body: "*"`는 query surface와 HTTP semantics를 숨길 수 있어 default 금지다.
|
||||
- additional binding collision과 ambiguous path template를 build에서 거절한다.
|
||||
- `.proto` annotation을 기본 source of truth로 삼고 external service config를
|
||||
병용하면 override precedence와 두 artifact digest를 고정한다.
|
||||
- `generate_unbound_methods`는 기본 금지하며 GET/DELETE body, path field type,
|
||||
path unescape mode와 PATCH FieldMask behavior를 fixture로 고정한다.
|
||||
- Buf generic breaking check가 custom HTTP option의 제품 의미까지 증명한다고
|
||||
간주하지 않는다. canonical route manifest와 generated OpenAPI의 method/path/
|
||||
body/query/additional-binding diff를 별도 gate로 검사한다.
|
||||
- `response_body` projection은 전체 response schema와 mapper binding을 별도로
|
||||
갖는다.
|
||||
- ProtoJSON JSON name/default/enum/int64/unknown policy를 고정한다.
|
||||
- request/response body와 URL ceiling은 transcoder 앞/뒤 모두 적용한다.
|
||||
|
||||
server-stream을 JSON array로 buffer해 반환하는 transcoder 동작을 realtime
|
||||
stream으로 간주하지 않는다. SSE/NDJSON/streaming JSON이 필요하면 별도 protocol
|
||||
ADR과 framing/content type/terminal contract를 만든다.
|
||||
|
||||
## 11. Auth, CSRF와 CORS
|
||||
|
||||
same-origin BFF를 권장한다. cross-origin이면 protocol별 exact profile이 필요하다.
|
||||
|
||||
Connect:
|
||||
|
||||
- POST와 optional GET
|
||||
- `Content-Type`, `Connect-Protocol-Version`, `Connect-Timeout-Ms`,
|
||||
selected compression/auth headers
|
||||
- custom unary trailer를 expose할 때 `Trailer-<Name>`
|
||||
|
||||
gRPC-Web:
|
||||
|
||||
- POST
|
||||
- `Content-Type`, `Grpc-Timeout`, `X-Grpc-Web`, `X-User-Agent`,
|
||||
selected auth headers
|
||||
- `Grpc-Status`, `Grpc-Message`, `Grpc-Status-Details-Bin` expose
|
||||
|
||||
REST Gateway:
|
||||
|
||||
- operation별 method/header/media
|
||||
- bearer 또는 cookie+CSRF profile
|
||||
- conditional/idempotency header allow/expose
|
||||
|
||||
공통:
|
||||
|
||||
- exact allow-origin, credential mode와 `Vary`
|
||||
- wildcard credential 금지
|
||||
- redirect login/HTML response 금지
|
||||
- Origin/Fetch Metadata/CSRF를 cookie unsafe method에서 검증
|
||||
- generated client/caller가 arbitrary metadata를 제출하지 못함
|
||||
- auth attach 뒤 endpoint/path/method/body digest 불변
|
||||
|
||||
## 12. Deadline, cancellation, retry와 command
|
||||
|
||||
```text
|
||||
total logical deadline
|
||||
= auth attach
|
||||
+ transport attempts/backoff
|
||||
+ body/frame read/decompression
|
||||
+ generated decode
|
||||
+ semantic validation
|
||||
+ mapper
|
||||
```
|
||||
|
||||
- Connect-Web call은 AbortSignal과 bounded timeout을 받는다.
|
||||
- local deadline과 wire timeout 중 더 짧은 값만 사용한다.
|
||||
- stream은 idle/total deadline 둘 다 갖는다.
|
||||
- abort가 backend command rollback을 의미하지 않는다.
|
||||
- generated/runtime interceptor retry와 Query retry를 중복하지 않는다.
|
||||
- `SAFE | IDEMPOTENT | KEYED_COMMAND`의 exact evidence가 있는 operation만 retry한다.
|
||||
- keyed command는 protocol과 무관하게 backend atomic idempotency/reconcile이
|
||||
필요하다.
|
||||
- gateway의 header/message mapping은 idempotency key를 전달할 뿐 durable
|
||||
dedupe, receipt, retention과 reconcile을 구현하지 않는다.
|
||||
- browser abort는 이미 commit된 command의 rollback 증거가 아니다. edge→upstream
|
||||
cancel/deadline 전파와 backend cooperative cancellation을 staging에서 검증한다.
|
||||
- protocol 장애를 이유로 Connect↔gRPC-Web↔REST command를 자동 replay하지 않는다.
|
||||
- read fallback도 새 logical operation으로 시작하고 old result를 cache에 쓰지 않는다.
|
||||
|
||||
## 13. Status와 error mapping
|
||||
|
||||
common safe vocabulary에 mapping하되 wire authority를 섞지 않는다.
|
||||
|
||||
| source | authoritative outcome |
|
||||
| --- | --- |
|
||||
| Connect unary | HTTP status + bounded Connect JSON error |
|
||||
| Connect stream | HTTP admission + final EndStream envelope |
|
||||
| gRPC-Web | HTTP admission + terminal grpc-status source |
|
||||
| REST Gateway | selected HTTP/envelope/problem profile |
|
||||
|
||||
mapping은 `UNAUTHENTICATED/AUTH_REQUIRED`, `PERMISSION_DENIED/FORBIDDEN`,
|
||||
`NOT_FOUND`, `CONFLICT/ABORTED`, validation, rate limit, unavailable,
|
||||
deadline/cancel을 공통 `AppFailure`로 투영한다. raw message, arbitrary Any/detail,
|
||||
metadata/trailer와 vendor error는 application/log에 전달하지 않는다.
|
||||
|
||||
같은 semantic backend error라도 transport별 status body가 다를 수 있으므로
|
||||
conformance suite가 최종 `AppFailure`와 retry/effect certainty가 같은지 검증한다.
|
||||
|
||||
## 14. Server State와 cache
|
||||
|
||||
- query key는 semantic application input에서만 파생한다.
|
||||
- service/method/protobuf bytes/REST URL을 key에 넣지 않는다.
|
||||
- generated message/Connect response를 cache하지 않고 mapper output만 admission한다.
|
||||
- transport retry가 있으면 Query retry는 off다.
|
||||
- scope/generation mismatch result는 폐기한다.
|
||||
- Connect GET/CDN cache, browser HTTP cache와 TanStack cache owner를 operation별로
|
||||
하나씩 명시한다.
|
||||
- stream event는 ordinary query result가 아니다.
|
||||
- finite stream aggregate는 terminal success 뒤 atomic commit한다.
|
||||
- long-running stream은 bounded reducer 또는 invalidate-only hint를 사용한다.
|
||||
|
||||
## 15. Security와 privacy
|
||||
|
||||
- descriptor/generated code는 신뢰된 source에서만
|
||||
- fixed provider/service/method/route
|
||||
- message/body/frame/decompressed/collection/depth ceiling
|
||||
- recursive schema와 Any type allowlist
|
||||
- frontend generated validation을 backend authorization으로 간주 금지
|
||||
- raw payload, ProtoJSON, debug stringifier, metadata/trailer/cursor/revision log 금지
|
||||
- request/response message에 credential을 넣지 않음
|
||||
- query cache/persistence에 generated message 없음
|
||||
- gateway가 unknown field/duplicate JSON key를 어떻게 처리하는지 fixture로 고정
|
||||
|
||||
## 16. Observability
|
||||
|
||||
허용:
|
||||
|
||||
```text
|
||||
semantic operation ID
|
||||
protocol/client runtime/provider profile ID
|
||||
descriptor/http-rule artifact version
|
||||
HTTP status group / safe RPC code
|
||||
attempt/duration/message-size/count bucket
|
||||
stream terminal/idle/gap/overflow bucket
|
||||
cache admission outcome
|
||||
```
|
||||
|
||||
금지:
|
||||
|
||||
- service request/response content
|
||||
- protobuf debug JSON/string
|
||||
- metadata/trailer/error message/detail
|
||||
- URL query/GET message
|
||||
- credential/idempotency/cursor/revision
|
||||
- resource/account identifier actual value
|
||||
|
||||
protocol별 metric을 비교할 때 semantic operation ID를 join key로 쓰고 raw
|
||||
service/method를 high-cardinality label로 사용하지 않는다.
|
||||
|
||||
## 17. Test와 provider evidence
|
||||
|
||||
### 17.1 Deterministic
|
||||
|
||||
- proto format/lint/breaking/descriptor/codegen digest
|
||||
- enum/oneof/presence/int64/time/bytes/unknown-field fixture
|
||||
- Connect unary JSON/binary success/error/media/status
|
||||
- Connect stream partial prefix, length, compression, EndStream, truncation
|
||||
- gRPC-Web binary/text frame/trailer/status matrix
|
||||
- REST annotation path/query/body/response mapping
|
||||
- ProtoJSON default/name/enum/int64/null/unknown behavior
|
||||
- retry/deadline/cancel/auth/idempotency
|
||||
- scope/generation/cache admission
|
||||
- generated import/bundle/removal
|
||||
|
||||
### 17.2 Actual provider/browser
|
||||
|
||||
- selected Connect/gRPC-Web server or Envoy/gateway version
|
||||
- exact CORS/preflight/auth/CSRF
|
||||
- HTTP/1.1/2 proxy buffering and stream flush
|
||||
- media, compression, terminal status/trailer preservation
|
||||
- message/body/time limit
|
||||
- Chromium/Firefox/WebKit cancel/stream/backpressure
|
||||
- REST Gateway N/N-1 and OpenAPI/descriptor coherence
|
||||
- browser abort→gateway context→upstream cancel/deadline propagation
|
||||
- HttpRule route manifest/OpenAPI/runtime response rewrite coherence
|
||||
- kill switch, rollback and dependency/proxy removal drill
|
||||
|
||||
memory fake/MSW만으로 provider conformance를 주장하지 않는다.
|
||||
|
||||
## 18. Rollout
|
||||
|
||||
```text
|
||||
product operation selected
|
||||
-> schema/provider/gateway owner
|
||||
-> immutable proto + descriptor
|
||||
-> codegen and semantic mapper
|
||||
-> provider-neutral adapter/fake
|
||||
-> actual gateway/browser conformance
|
||||
-> AVAILABLE_NOT_COMPOSED
|
||||
-> bootstrap TrafficAdmission=DISABLED
|
||||
-> COMPOSED
|
||||
-> read-only shadow
|
||||
-> canary
|
||||
-> enabled
|
||||
```
|
||||
|
||||
- 처음에는 safe unary read 하나만 선택한다.
|
||||
- shadow result는 UI/cache에 쓰지 않는다.
|
||||
- Connect와 gRPC-Web을 동시에 canary하지 않는다.
|
||||
- server stream은 unary와 별도 gate다.
|
||||
- command는 backend idempotency/reconcile 뒤 별도 gate다.
|
||||
- REST fallback은 사전 등록된 read operation만 새 logical query로 실행한다.
|
||||
- rollback은 frontend/generated descriptor/gateway/backend를 coherent set으로 한다.
|
||||
|
||||
## 19. Removal
|
||||
|
||||
1. 신규 call/stream admission을 닫는다.
|
||||
2. query/stream cancel, command effect reconcile.
|
||||
3. cache/reducer/invalidation listener를 clear한다.
|
||||
4. operation/schema/mapper/provider profile을 제거한다.
|
||||
5. generated source, descriptor/proto input과 codegen config를 제거한다.
|
||||
6. Connect/gRPC runtime dependency와 proxy/transcoder route를 제거한다.
|
||||
7. backend method/HTTP binding은 N/N-1 client window 뒤 retirement한다.
|
||||
8. bundle, SBOM, lockfile, proxy config와 source reference 0을 증명한다.
|
||||
|
||||
## 20. 구현 work package
|
||||
|
||||
| 순서 | package | exit |
|
||||
| --- | --- | --- |
|
||||
| PB-01 | schema governance | authenticated module, Buf lint/breaking, descriptor/digest |
|
||||
| PB-02 | generated boundary | deterministic TS generation, private imports, mapper |
|
||||
| PB-03 | Connect unary | exact JSON/binary profile, error/deadline/cancel |
|
||||
| PB-04 | gRPC-Web unary | selected runtime/proxy/frame/status conformance |
|
||||
| PB-05 | bounded server stream | queue/idle/total/terminal/sequence/resume |
|
||||
| PB-06 | REST Gateway | selected kind, HTTP annotation/ProtoJSON/error/cache fixture |
|
||||
| PB-07 | operations | browser/provider evidence, canary/kill/rollback/removal |
|
||||
|
||||
PB-03~06은 제품에서 선택한 branch만 구현한다. “미래 대비”로 모두 설치하지 않는다.
|
||||
|
||||
## 21. 완료 기준
|
||||
|
||||
- [ ] Protobuf, client runtime, wire protocol과 gateway 축이 분리돼 있다.
|
||||
- [ ] 한 operation은 한 active transport/provider profile만 가진다.
|
||||
- [ ] descriptor/codegen/runtime/gateway artifact가 release digest에 binding된다.
|
||||
- [ ] generated type이 adapter 밖으로 나오지 않는다.
|
||||
- [ ] ProtoJSON과 binary compatibility policy가 각각 닫혀 있다.
|
||||
- [ ] Connect unary/stream과 gRPC-Web decoder가 서로의 terminal 규칙을 섞지 않는다.
|
||||
- [ ] REST Gateway가 envelope/cache/idempotency를 자동 제공한다고 가장하지 않는다.
|
||||
- [ ] client/bidi streaming을 browser 공통 capability로 표시하지 않는다.
|
||||
- [ ] actual proxy와 세 browser evidence가 있다.
|
||||
- [ ] command fallback/replay가 backend effect certainty를 우회하지 않는다.
|
||||
- [ ] coherent rollback과 complete removal drill이 통과한다.
|
||||
|
||||
## 22. 규범·공식 근거
|
||||
|
||||
- [Connect protocol reference](https://connectrpc.com/docs/protocol/)
|
||||
- [Connect-Web protocol selection](https://connectrpc.com/docs/web/choosing-a-protocol/)
|
||||
- [Connect-Web code generation](https://connectrpc.com/docs/web/generating-code/)
|
||||
- [Connect and gRPC-Web CORS](https://connectrpc.com/docs/cors/)
|
||||
- [gRPC-Web protocol delta](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md)
|
||||
- [Official grpc-web runtime](https://github.com/grpc/grpc-web)
|
||||
- [Envoy gRPC-Web filter](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_web_filter.html)
|
||||
- [gRPC HTTP status fallback mapping](https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md)
|
||||
- [Envoy timeout configuration](https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts.html)
|
||||
- [Protocol Buffers language guide](https://protobuf.dev/programming-guides/proto3/)
|
||||
- [ProtoJSON format](https://protobuf.dev/programming-guides/json/)
|
||||
- [Buf breaking changes](https://buf.build/docs/breaking/)
|
||||
- [gRPC-Gateway introduction](https://grpc-ecosystem.github.io/grpc-gateway/docs/tutorials/introduction/)
|
||||
- [gRPC-Gateway customization](https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/customizing_your_gateway/)
|
||||
- [Google API HTTP annotation](https://github.com/googleapis/googleapis/blob/master/google/api/http.proto)
|
||||
- [Envoy gRPC-JSON transcoder](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,7 @@ RP-04 이후 route runtime과 RP-06 page/form platform에는 다음 장점이
|
||||
- reference feature의 list/detail/create/status route가 네 template variation을
|
||||
production composition에서 실행한다.
|
||||
|
||||
platform route 계약, `src/features/installed-feature-contracts.js`,
|
||||
platform route 계약, `src/features/installed-feature-contracts.ts`,
|
||||
`src/features/installed-feature-runtimes.tsx`의 완전성은 TypeScript와 registry
|
||||
negative fixture가 함께 검사한다. params/search codec, loading/error surface,
|
||||
access, title, navigation, chunk ID는
|
||||
@@ -37,6 +37,13 @@ access, title, navigation, chunk ID는
|
||||
manifest의 dynamic entry는 release manifest route chunk map과 검증되며,
|
||||
`ChunkRecoveryBoundary`는 일반 render error와 chunk rejection을 분리한다.
|
||||
|
||||
route ID와 parsed input type은 `route-contract.ts`, parse/canonical URL 생성은
|
||||
`route-codecs.ts`, React context/provider/hook은 `route-input.tsx`가 소유한다.
|
||||
router는 codec 결과를 provider에 넣고 lazy feature page는 좁은
|
||||
`useRouteInput()`만 소비한다. 이 분리로 feature runtime이 자신을 load하는
|
||||
`app-router.tsx`를 역참조하지 않으며, 같은 유형의 TypeScript 순환 의존은
|
||||
architecture graph fixture가 거절한다.
|
||||
|
||||
template는 데이터를 가져오지 않는다. reference page controller가 route input과
|
||||
application input을 query/form facade에 연결하고, template에는 render할 slot과
|
||||
안전한 callback만 전달한다. 이 분리는
|
||||
@@ -391,10 +398,8 @@ page view
|
||||
|
||||
```tsx
|
||||
export function ResourceListRoute() {
|
||||
const input = useRouteInput(resourceListRoute);
|
||||
if (!input.ok) return <InvalidRouteSurface failure={input.error} />;
|
||||
|
||||
return <ResourceListController input={input.value} />;
|
||||
const input = useRouteInput();
|
||||
return <ResourceListController input={input} />;
|
||||
}
|
||||
|
||||
function ResourceListController({ input }: ResourceListControllerProps) {
|
||||
@@ -403,7 +408,8 @@ function ResourceListController({ input }: ResourceListControllerProps) {
|
||||
}
|
||||
```
|
||||
|
||||
route parse boundary와 controller component를 분리하므로 controller hook은
|
||||
router의 parse/invalid-route boundary가 성공한 `ParsedRouteInput`만 provider에
|
||||
넣고, page controller는 그 context를 바로 읽는다. 따라서 controller hook은
|
||||
조건부로 호출되지 않는다.
|
||||
|
||||
controller가 소유하는 것:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,7 @@ and the
|
||||
rather than adding another independent route or data-loading convention.
|
||||
|
||||
1. Add a serializable contribution under the feature ownership boundary and
|
||||
install it through `src/features/installed-feature-contracts.js`.
|
||||
install it through `src/features/installed-feature-contracts.ts`.
|
||||
2. Add the lazy component and route codecs through
|
||||
`src/features/installed-feature-runtimes.tsx`.
|
||||
3. Compose feature application inputs and outbound gateways only through
|
||||
@@ -72,8 +72,8 @@ rather than adding another independent route or data-loading convention.
|
||||
5. Add component behavior, all-engine E2E, automated axe, and signed manual
|
||||
route evidence.
|
||||
6. Run `test:sample-removal` to prove the generic starter typechecks, passes
|
||||
architecture/registry/tests/home smoke, and builds without the complete
|
||||
reference feature.
|
||||
architecture/registry/tests/coverage/source-evidence/home smoke, and builds
|
||||
without the complete reference feature.
|
||||
|
||||
Theme preference is the public `COLOR_SCHEME` storage contract. Authentication
|
||||
tokens and other secrets remain forbidden storage keys.
|
||||
|
||||
@@ -4,21 +4,27 @@
|
||||
|
||||
이 문서는 다음 질문에 대한 저장소 표준을 정의한다.
|
||||
|
||||
- JavaScript를 어떤 순서로 TypeScript로 전환하는가.
|
||||
- TypeScript-only 경계를 어떻게 유지하고 JavaScript 재유입을 막는가.
|
||||
- local, URL, server, form, global, persisted 상태를 어디에 둬야 하는가.
|
||||
- React 화면이 application use case와 TanStack Query를 어떻게 사용해야 하는가.
|
||||
- HTTP, retry, auth, error, validation, logging의 책임을 어떻게 나누는가.
|
||||
- 새 query, mutation, form을 추가할 때 어떤 파일과 테스트가 필요한가.
|
||||
|
||||
이 문서는 목표 설계다. 현재 구현 상태는
|
||||
[프론트엔드 플랫폼 역량 재검토](./frontend-platform-capability-review.md)를 따른다.
|
||||
이 문서는 현재 구현된 TypeScript 경계와 상태/데이터 흐름의 저장소 표준이다.
|
||||
세부 역량의 구현 상태는
|
||||
[프론트엔드 플랫폼 역량 재검토](./frontend-platform-capability-review.md)를
|
||||
따른다. memory query cache, Web Storage와 탭 간 invalidation의 세부 protocol은
|
||||
[Client cache and browser storage platform](./client-cache-and-storage.md)에
|
||||
기록한다.
|
||||
|
||||
## 2. TypeScript 전환 원칙
|
||||
|
||||
### 2.1 왜 전환하는가
|
||||
|
||||
현재 `strict + allowJs + checkJs`는 JavaScript 상태에서 유용한 안전망이다. 그러나
|
||||
JSDoc cast가 늘어나면 다음 계약을 정확히 닫기 어렵다.
|
||||
초기 기준선의 `strict + allowJs + checkJs`는 JavaScript 상태에서 유용한
|
||||
중간 안전망이었다. 현재는 product source, test, Node script와 지원되는 config를
|
||||
TS/TSX로 전환하고 `allowJs: false`로 닫았다. JSDoc cast 대신 실제 TypeScript
|
||||
타입으로 다음 계약을 검사한다.
|
||||
|
||||
- `RouteId`, `OperationId`, `ErrorCode`, `StorageKey`, `TelemetryEvent`
|
||||
- `Result<T, E>`와 discriminated failure union
|
||||
@@ -31,16 +37,17 @@ JSDoc cast가 늘어나면 다음 계약을 정확히 닫기 어렵다.
|
||||
TypeScript 전환 목적은 확장자 변경이 아니라 이 계약을 컴파일 단계에서
|
||||
검증하는 것이다.
|
||||
|
||||
### 2.2 전환 전에 고칠 도구
|
||||
### 2.2 현재 도구 경계
|
||||
|
||||
다음 변경이 첫 브랜치에서 완료되기 전에는 source rename을 시작하지 않는다.
|
||||
전환 이후에도 다음 조건을 blocking gate로 유지한다.
|
||||
|
||||
1. ESLint가 `js`, `jsx`, `mjs`, `ts`, `tsx`, `mts`를 모두 검사한다.
|
||||
1. ESLint가 product와 negative fixture의 `ts`/`tsx`를 모두 검사한다.
|
||||
2. React Hooks 규칙을 추가하고 TypeScript/ESLint parser와 JSX accessibility
|
||||
도구는 설치된 compiler/linter의 공식 peer 범위 안에서 선택한다.
|
||||
3. dependency-cruiser의 extension과 resolver가 TS/TSX를 포함한다.
|
||||
4. `scripts/check-registries.mjs`가 TS/TSX를 검색한다.
|
||||
5. `config/contracts/registry-governance.json`의 경로 갱신 절차를 만든다.
|
||||
4. `scripts/check-registries.ts`가 TS/TSX를 검색한다.
|
||||
5. `config/contracts/registry-governance.json`과 승인 baseline은 `.ts/.tsx`
|
||||
source 경로를 사용한다.
|
||||
6. Vite, Vitest, Playwright, scripts, source, tests를 각각 typecheck한다.
|
||||
7. invalid type fixture가 TS migration 후에도 “실패해야 통과”하는지 확인한다.
|
||||
8. architecture/security/registry gate가 TS fixture 위반을 실제로 잡는 negative test를
|
||||
@@ -56,36 +63,34 @@ tsconfig.test.json
|
||||
tsconfig.json # project references only
|
||||
```
|
||||
|
||||
`tsconfig.base.json`의 초기 핵심 옵션:
|
||||
`tsconfig.base.json`의 핵심 옵션:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"useUnknownInCatchVariables": true,
|
||||
"noImplicitOverride": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"allowJs": false,
|
||||
"checkJs": false,
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
실제 TypeScript 7/Vite 호환 옵션은 설치된 공식 문서와 빌드 결과를 기준으로
|
||||
확정한다. 옵션을 한꺼번에 켜서 수백 개 예외를 만들지 말고, 각 단계에서 새
|
||||
예외를 금지한다.
|
||||
Node가 직접 실행하는 script/config project는 추가로 `NodeNext`,
|
||||
`verbatimModuleSyntax`, `rewriteRelativeImportExtensions`와
|
||||
`erasableSyntaxOnly`를 적용한다. Vite app/test project는 Bundler resolution을
|
||||
사용하되 저장소 내부 상대 import에 실제 `.ts/.tsx` 확장자를 기록한다.
|
||||
|
||||
현재 저장소의 VD-01 결정은
|
||||
[TypeScript 7과 ESLint 10의 점진적 전환 도구](./decisions/VD-01-typescript-lint-tooling.md)에
|
||||
기록돼 있다. app, Node scripts/config, tests는 각각 독립된 project로
|
||||
typecheck하며 JS에는 `checkJs`, TS에는 `strict`를 적용한다. TypeScript 7을
|
||||
아직 지원하지 않는 parser plugin을 강제 설치하지 않고 Babel parser는 lint
|
||||
syntax/import/security 검사, `tsc`는 type semantics를 소유한다.
|
||||
기록돼 있다. app, Node scripts/config, tests는 각각 독립된 strict project로
|
||||
typecheck한다. TypeScript 7을 아직 지원하지 않는 parser plugin을 강제
|
||||
설치하지 않고 Babel parser는 lint syntax/import/security 검사, `tsc`는 type
|
||||
semantics를 소유한다.
|
||||
|
||||
### 2.3 전환 순서
|
||||
### 2.3 완료된 전환 순서
|
||||
|
||||
| 단계 | 대상 | 이유 | 종료 조건 |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -97,37 +102,29 @@ syntax/import/security 검사, `tsc`는 type semantics를 소유한다.
|
||||
| 5 | bootstrap/composition | 누락 dependency를 컴파일로 검출 | 실제 composition type test 통과 |
|
||||
| 6 | React providers/controllers/routes | typed application API 소비 | route/runtime map 완전성 검사 |
|
||||
| 7 | primitives/pages/templates | component API와 variant를 닫음 | stories/component tests typecheck |
|
||||
| 8 | tests/scripts/config | 우회 없는 전체 저장소 | source `allowJs` 제거 가능 |
|
||||
| 8 | tests/scripts/config | 우회 없는 전체 저장소 | `allowJs: false`, TS-only architecture gate |
|
||||
|
||||
각 단계는 빌드 가능한 작은 커밋으로 유지한다. JavaScript와 TypeScript가 공존하는
|
||||
동안에는 [공식 JavaScript migration 가이드](https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html)의
|
||||
점진적 방식을 사용한다.
|
||||
이 순서는 기존 계약을 보존하며 수행한 migration 기록이다. 현재 실행 코드와
|
||||
`tests/fixtures/**`에는 JavaScript와 TypeScript를 공존시키지 않는다. 실패를
|
||||
의도한 fixture도 실제 `.ts/.tsx` 입력이며 product import 대상은 아니다.
|
||||
|
||||
### 2.4 기본 type 계약
|
||||
|
||||
다음 형태를 core application에 둔다.
|
||||
공통 success/failure carrier는 `src/application/result.ts`에 한 번만 둔다.
|
||||
|
||||
```ts
|
||||
export type Ok<T> = Readonly<{ ok: true; value: T }>;
|
||||
export type Err<E> = Readonly<{ ok: false; error: E }>;
|
||||
export type Result<T, E> = Ok<T> | Err<E>;
|
||||
import type { AppFailure } from "../contracts/errors.ts";
|
||||
|
||||
export type AppFailure =
|
||||
| Readonly<{ kind: "unauthenticated"; code: "AUTH_REQUIRED"; traceId?: string }>
|
||||
| Readonly<{ kind: "forbidden"; code: "FORBIDDEN"; traceId?: string }>
|
||||
| Readonly<{ kind: "not-found"; code: "NOT_FOUND"; traceId?: string }>
|
||||
| Readonly<{ kind: "conflict"; code: "CONFLICT"; traceId?: string }>
|
||||
| Readonly<{
|
||||
kind: "validation";
|
||||
code: "VALIDATION_FAILED";
|
||||
fields: Readonly<Record<string, readonly string[]>>;
|
||||
traceId?: string;
|
||||
}>
|
||||
| Readonly<{ kind: "rate-limited"; code: "RATE_LIMITED"; retryAt?: Date }>
|
||||
| Readonly<{ kind: "unavailable"; code: "UNAVAILABLE"; retryable: boolean }>
|
||||
| Readonly<{ kind: "unexpected"; code: "UNEXPECTED"; traceId?: string }>;
|
||||
export type Result<Value, Failure = AppFailure> =
|
||||
| Readonly<{ ok: true; value: Value }>
|
||||
| Readonly<{ ok: false; error: Failure }>;
|
||||
```
|
||||
|
||||
`AppFailure.kind`의 `FailureKind`는 `keyof typeof ERROR_REGISTRY`에서 파생한다.
|
||||
따라서 registry에 없는 failure kind는 컴파일되지 않으며, application,
|
||||
query/form controller와 feature API가 같은 default failure contract를 사용한다.
|
||||
기존 HTTP 경계의 `ApiFailure` 이름은 `AppFailure`의 호환 alias로만 유지한다.
|
||||
|
||||
원칙:
|
||||
|
||||
- adapter에서 받은 `unknown`은 adapter 경계에서 parse한다.
|
||||
@@ -139,6 +136,28 @@ export type AppFailure =
|
||||
- `as`, non-null assertion, `any`는 경계에서 근거가 있을 때만 사용하고 lint
|
||||
예외에 사유를 기록한다.
|
||||
|
||||
### 2.5 typed feature input contribution
|
||||
|
||||
generic application은 concrete feature를 import하지 않는다. 대신 비어 있는
|
||||
`ApplicationFeatureInputs`를 소유하고, 설치되는 feature의 application API가
|
||||
module augmentation으로 ID와 input shape를 기여한다.
|
||||
|
||||
```ts
|
||||
export interface ApplicationFeatureInputs {}
|
||||
|
||||
declare module "../../../application/ports/in/application-api.ts" {
|
||||
interface ApplicationFeatureInputs {
|
||||
"reference-feature": ReferenceFeatureInput;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ApplicationFeatureId`는 이 interface의 string key에서 파생하며,
|
||||
`features.get(id)`는 해당 key의 정확한 input type을 반환한다. 잘못된 ID, 누락된
|
||||
input method와 잘못된 method signature는 negative type fixture가 거절한다.
|
||||
runtime `has` type guard와 설치 누락 예외는 JavaScript나 외부 동적 입력 경계도
|
||||
fail-closed로 유지한다.
|
||||
|
||||
## 3. 상태 소유권
|
||||
|
||||
### 3.1 상태 분류표
|
||||
@@ -204,6 +223,13 @@ type PersistedRecord<T> = Readonly<{
|
||||
- server state persistence와 offline mutation queue는 별도 project-selected
|
||||
adapter다.
|
||||
|
||||
현재 `StoragePort` 구현은 registry가 선언한 local/session backend,
|
||||
`color-scheme-v1`/`opaque-string-v1` closed value codec, schema version, TTL과
|
||||
16,384-byte hard cap을 적용한다. `QUERY_PERSISTENCE`는
|
||||
`disabled`/`sensitive-forbidden`이고 installed query registry도
|
||||
`persistence: "disabled"`만 허용한다. 기존 IndexedDB reference runtime은
|
||||
`AVAILABLE_NOT_COMPOSED`이며 TanStack hydration에는 연결하지 않는다.
|
||||
|
||||
## 4. 표준 데이터 호출 경로
|
||||
|
||||
```mermaid
|
||||
@@ -286,13 +312,11 @@ export function useResourcesQuery(input: ListResourcesInput) {
|
||||
```ts
|
||||
export function useCreateResourceMutation() {
|
||||
const application = useApplication();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (command: CreateResourceCommand) =>
|
||||
application.resources.create(command).then(unwrapResult),
|
||||
onSuccess: () =>
|
||||
queryClient.invalidateQueries({ queryKey: resourceKeys.all }),
|
||||
return useApplicationMutation({
|
||||
execute: (command: CreateResourceCommand) =>
|
||||
application.resources.create(command),
|
||||
invalidate: [RESOURCE_INVALIDATION_TOPIC],
|
||||
});
|
||||
}
|
||||
```
|
||||
@@ -316,6 +340,32 @@ export function useCreateResourceMutation() {
|
||||
optimistic update는 기본값이 아니다. 서버 규칙을 확실히 재현할 수 있고 rollback이
|
||||
안전한 mutation에만 사용한다.
|
||||
|
||||
### 4.4 탭 간 query invalidation
|
||||
|
||||
현재 installed query contract는 namespace와 별도로 opaque
|
||||
`invalidationTopic`, topic `version`, `crossContext: "invalidate-only"`와
|
||||
`persistence: "disabled"`를 등록한다.
|
||||
`useApplicationMutation`은 server mutation 성공 뒤
|
||||
`QueryInvalidationCoordinator`에 topic을 전달한다. coordinator는 local
|
||||
TanStack namespace를 active-query mode로 invalidate한 다음
|
||||
2,048-byte 이하의 payload/query-key-free hint만 다른 context에 발행한다.
|
||||
|
||||
```text
|
||||
server mutation success
|
||||
-> registered topic
|
||||
-> local namespace invalidate
|
||||
-> BroadcastChannel hint
|
||||
-> failure: localStorage pulse
|
||||
-> failure: DEGRADED_LOCAL_ONLY
|
||||
```
|
||||
|
||||
receiver는 exact protocol/topic/release cache epoch를 검증하고 self echo,
|
||||
duplicate와 stale out-of-order event를 버린다. sequence gap이면 모든 등록
|
||||
namespace를 stale 처리해 다시 읽는다. remote event는
|
||||
`removeQueries`, `resetQueries` 또는 `queryClient.clear()` 권한을 갖지 않는다.
|
||||
logout/account transition은 이후 session/account epoch와 local lifecycle
|
||||
authority를 추가해야 하며 현재 release epoch만 조립돼 있다.
|
||||
|
||||
## 5. HTTP client 책임
|
||||
|
||||
### 5.1 목표 파이프라인
|
||||
@@ -625,3 +675,7 @@ backend message와 알 수 없는 path는 field copy로 사용하지 않는다.
|
||||
- failure와 validation의 각 계층이 typed mapper로 분리된다.
|
||||
- common UI copy, locale formatter와 direction이 typed i18n facade를 통과한다.
|
||||
- query/mutation/form recipe만으로 새 기능을 만들 수 있다.
|
||||
- query mutation invalidation은 registry topic을 통해 local cache와 다른 tab에
|
||||
연결되고 query key/data는 wire에 노출되지 않는다.
|
||||
- query persistence는 명시적으로 disabled이고 기존 IndexedDB runtime과 암묵적으로
|
||||
조합되지 않는다.
|
||||
|
||||
Reference in New Issue
Block a user