Files
tech-log-frontend/docs/architecture/2026-07-30-frontend-runtime-capability-repository-aligned-implementation-closed-deep-design.md
T

5958 lines
154 KiB
Markdown

# 프론트엔드 런타임 Capability 저장소 정합형 구현 결정 폐쇄 상세 설계
- 작성일: 2026-07-30
- 상태: 구현 결정 정본 v1
- 대상 저장소: `clean-architecture-frontend-template`
- 기준 저장소 스냅샷: 제공된 Repomix 전체 저장소 표현
- 상위 설계:
- `2026-07-30-frontend-production-capability-repository-aligned-deep-design.md`
- 계약 정본:
- 별도 API·이벤트 계약 관리 저장소에서 발행한 immutable TypeScript contract package
- 기준 기술:
- Node.js 24 major
- pnpm 11
- TypeScript strict / `allowJs: false`
- React 19
- React Router 7 data mode
- TanStack Query 5
- Zod 4
- Vite 8
- 문서 규칙:
- **MUST**: 구현자가 변경할 수 없는 기본 결정
- **MUST NOT**: 존재하면 설계 위반
- **MAY**: 명시적으로 선택된 capability에만 허용
- 본 문서의 exact literal, 상태, 상한, 순서와 source path는 예시가 아니라 구현 정본이다.
---
## 목차
- [0. 문서 권한과 제외 범위](#0-문서-권한과-제외-범위)
- [1. 현재 저장소 기준선](#1-현재-저장소-기준선)
- [2. 구현 결정 폐쇄 표](#2-구현-결정-폐쇄-표)
- [3. 목표 런타임 구조와 lifecycle](#3-목표-런타임-구조와-lifecycle)
- [4. 외부 계약 package 소비 경계](#4-외부-계약-package-소비-경계)
- [5. Contract Set과 release coherence](#5-contract-set과-release-coherence)
- [6. Runtime Config와 boot](#6-runtime-config와-boot)
- [7. Fetch HTTP execution runtime](#7-fetch-http-execution-runtime)
- [8. Retry·cancellation·mutation effect certainty](#8-retrycancellationmutation-effect-certainty)
- [9. Router](#9-router)
- [10. Query lifecycle과 cache](#10-query-lifecycle과-cache)
- [11. Mutation lifecycle](#11-mutation-lifecycle)
- [12. Cross-context invalidation](#12-cross-context-invalidation)
- [13. Realtime product composition](#13-realtime-product-composition)
- [14. Connection·Reconnect·Resume](#14-connectionreconnectresume)
- [15. Event validation과 subscription cleanup](#15-event-validation과-subscription-cleanup)
- [16. Generic Web Worker](#16-generic-web-worker)
- [17. Service Worker lifecycle](#17-service-worker-lifecycle)
- [18. Cache Storage와 fetch policy](#18-cache-storage와-fetch-policy)
- [19. Offline Command와 Background Sync](#19-offline-command와-background-sync)
- [20. 공통 start·stop·dispose](#20-공통-startstopdispose)
- [21. Security와 privacy](#21-security와-privacy)
- [22. Runtime observability](#22-runtime-observability)
- [23. 구현 검증](#23-구현-검증)
- [24. Source 배치와 migration](#24-source-배치와-migration)
- [25. 구현 순서와 stop condition](#25-구현-순서와-stop-condition)
- [26. 완료 기준](#26-완료-기준)
- [27. 위험 등록부](#27-위험-등록부)
- [28. Normative decision ledger](#28-normative-decision-ledger)
- [부록 A. Numeric constants](#부록-a-numeric-constants)
- [부록 B. 금지 구현 목록](#부록-b-금지-구현-목록)
- [부록 C. 플랫폼 참고 기준](#부록-c-플랫폼-참고-기준)
---
## 0. 문서 권한과 제외 범위
### 0.1 목적
이 문서는 다음 프론트엔드 런타임 capability를 현재 저장소 구조에 맞춰 구현할 때 개발자가 다시 선택하거나 질문할 지점을 제거한다.
```text
Fetch HTTP Client
Runtime Config
Router
Query / Mutation
Connection / Reconnect / Resume
Event Validation
Subscription Cleanup
Web Worker
Service Worker
Background Sync
```
상위 저장소 정합형 설계가 방향과 책임을 고정했다면, 본 문서는 다음을 추가로 고정한다.
- exact source path;
- exact public type shape;
- exact lifecycle state;
- exact startup/shutdown order;
- exact timeout, queue, payload와 retry ceiling;
- exact optional capability selection과 removal 절차;
- external contract package를 저장소 runtime에 연결하는 단일 경로;
- browser lifecycle과 session/account transition ordering;
- failure union과 mutation uncertainty 처리;
- 구현 중 허용되지 않는 fallback;
- 구현 완료를 판정할 deterministic behavior case.
### 0.2 본 문서가 대체하는 범위
상위 설계의 다음 절은 본 문서가 더 구체적인 정본이다.
| 상위 범위 | 본 문서 정본 |
| --- | --- |
| Runtime Config와 boot coherence | §5–§6 |
| Fetch HTTP Client | §4, §7–§8 |
| Router | §9 |
| Query와 Mutation | §10–§12 |
| Realtime 공통 경계 | §13–§15 |
| Web Worker | §16 |
| Service Worker | §17–§18 |
| Offline Command와 Background Sync | §19 |
| optional capability 조립 | §3, §17.3, §24 |
| lifecycle·security·observability | §20–§22 |
| source 배치와 구현 순서 | §24–§25 |
상위 설계와 본 문서가 충돌하면 위 범위에서는 본 문서를 따른다.
### 0.3 API·이벤트 계약 관련 명시적 제외
다음은 별도 API·이벤트 계약 관리 저장소가 소유한다. 본 문서에서 다시 정의하거나 frontend local schema로 복제하지 않는다.
```text
OpenAPI / AsyncAPI 원문
operation method/path/status/media semantics
Problem Details type/code/extensions
pagination cursor wire shape
idempotency server semantics와 retention
event type/envelope/payload schema
replay/cursor/snapshot server protocol
provider compatibility와 deprecation/sunset
backend controller/event publisher conformance
contract package 생성·발행·서명·provenance
```
본 저장소가 소유하는 것은 다음뿐이다.
```text
immutable TypeScript contract package pin
package manifest/digest 검증
frontend-owned normalized descriptor interface
runtime validator 호출
wire DTO -> application value mapper
HTTP/Query/Realtime runtime behavior
contractSet와 frontend release coherence
unknown/future contract input의 safe failure
```
외부 package의 의미가 부족하거나 서로 모순되면 frontend가 임의 의미를 보충하지 않는다. 해당 feature contribution을 composition하지 않고 `CONTRACT_CONTRIBUTION_INVALID`로 종료한다.
### 0.4 CI 명시적 비범위
다음은 별도 CI/CD 플랫폼이 소유하며 본 문서에서 설계하지 않는다.
- workflow YAML;
- merge/release stage 구성;
- gate ID와 promotion formula;
- artifact retention service;
- package publication identity;
- vulnerability/signing/SBOM pipeline;
- deployment/canary controller;
- CI evidence aggregation.
§23은 코드가 만족해야 할 **행동 검증 케이스**만 정의한다. 어느 pipeline에서 실행할지는 정의하지 않는다.
### 0.5 정본 우선순위
충돌 시 다음 순서를 사용한다.
1. 현재 저장소 source와 실행 가능한 test가 증명하는 사실;
2. 별도 계약 저장소에서 발행한 exact immutable package;
3. 본 문서의 MUST/MUST NOT와 decision ledger;
4. 상위 저장소 정합형 설계;
5. 기존 일반 greenfield 설계;
6. 예시 코드와 설명 문구.
예시가 public type과 다르면 public type이 정본이다. 두 설정이 같은 의미를 서로 다르게 지정하면 precedence를 만들지 않고 startup을 실패시킨다.
---
## 1. 현재 저장소 기준선
### 1.1 물리 구조
현재 저장소의 canonical source root는 다음과 같다.
```text
src/
contracts/ # 저장소 내부 registry와 provider-neutral runtime contract
application/ # input/output port, use case, policy, view model
adapters/ # HTTP, query cache, storage, realtime, web-push 등 concrete adapter
presentation/ # React inbound adapter, Router, Query hook, UI
bootstrap/ # 유일한 composition root와 boot
features/ # 제거 가능한 vertical feature
```
다음 신규 parallel root는 만들지 않는다.
```text
src/platform
src/service-worker
src/workers
src/infrastructure
src/api
src/shared/api
```
### 1.2 계층 방향
현재 `config/architecture/layers.json`의 방향을 유지한다.
```text
domain -> domain only
application -> application, domain, contracts
presentation -> presentation, application, domain, contracts
adapters -> adapters, application, domain, contracts
bootstrap -> src 전체
```
추가 불변조건:
```text
presentation MUST NOT import adapters
application MUST NOT import React, Router, TanStack Query, browser API
adapters MUST NOT import presentation/bootstrap
feature presentation MUST call application input or approved inbound bridge
bootstrap alone selects concrete runtime
```
### 1.3 현재 구현을 재사용하는 항목
다음 source를 제거하거나 동일 기능으로 다시 만들지 않는다.
| Capability | 현재 source | 판정 |
| --- | --- | --- |
| Runtime Config schema/load | `src/bootstrap/runtime-config-schema.ts`, `load-runtime-config.ts` | 보강 |
| Release manifest/load | `src/bootstrap/load-release-manifest.ts` | V2 migration |
| composition root | `src/bootstrap/create-runtime-composition.ts`, `composition-root.ts` | 확장 |
| bounded HTTP response | `src/adapters/http/bounded-json.ts` | 공통 bounded body reader로 확장 |
| Fetch client | `src/adapters/http/client.ts` | execution V3 보강 |
| request builder | `src/adapters/http/request-builder.ts` | package descriptor bridge 연결 |
| REST registry/profile | `src/contracts/api-operations.ts`, `rest-profiles.ts` | external contribution normalization으로 migration |
| Query adapter | `src/presentation/adapters/query/application-query.ts` | 유지·보강 |
| Query cache coordinator | `src/adapters/query-cache/*` | scope/invalidation 보강 |
| Router runtime | `src/presentation/routes/*` | 유지 |
| realtime registry/runtime | `src/contracts/realtime-*`, `src/adapters/realtime/*` | 제품 조립만 추가 |
| Web Push worker handler | `src/adapters/web-push/service-worker-runtime.ts` | 단일 SW entry handler로 재사용 |
| IndexedDB reference runtime | `src/adapters/storage/indexeddb/*` | offline dataset backend로 재사용 |
| OPFS Worker | `src/adapters/storage/opfs/*` | generic CPU Worker와 분리 유지 |
| Cache Storage reference | `src/adapters/cache-storage/*` | static asset handler에서 재사용 |
### 1.4 현재 삭제하지 않는 compatibility surface
다음 public/runtime surface는 migration 동안 유지한다.
- existing reference feature route와 local fixture;
- current `ApiOperationV2` reader;
- current runtime config V1 reader;
- current release manifest V1 parser;
- current query key and invalidation contracts;
- current RT-01~RT-04 common runtime;
- current browser file/storage reference runtime.
새 writer가 V2를 사용하도록 먼저 전환한 뒤 V1 reader를 제거한다. 같은 release에서 source shape, manifest shape와 runtime behavior를 동시에 big-bang 교체하지 않는다.
### 1.5 현재 상태 taxonomy
이 문서는 저장소 기존 상태 literal을 유지한다.
```text
COMPOSED
AVAILABLE_NOT_COMPOSED
DESIGNED_NOT_IMPLEMENTED
NOT_SELECTED
PLATFORM_LIMITED
```
현재 주요 판정:
| Capability | 현재 | 본 문서 목표 |
| --- | --- | --- |
| Runtime Config | `COMPOSED` V1 | V2 bounded boot |
| Release coherence | `COMPOSED` scalar contract version | contractSet V2 |
| HTTP reference path | `COMPOSED` | execution V3 |
| Router | `COMPOSED` | ownership closed |
| Query memory cache | `COMPOSED` | scope/result/invalidation closed |
| Query persistence | `NOT_SELECTED` | 계속 disabled |
| Realtime common runtime | `AVAILABLE_NOT_COMPOSED` | 그대로 재사용 |
| Product realtime | `NOT_SELECTED` | explicit contribution 필요 |
| Generic CPU Worker | `NOT_SELECTED` | opt-in runtime |
| OPFS Worker | `AVAILABLE_NOT_COMPOSED` | 별도 유지 |
| Cache Storage | `AVAILABLE_NOT_COMPOSED` | static asset only |
| PWA Service Worker lifecycle | `DESIGNED_NOT_IMPLEMENTED` | reference runtime |
| Offline command queue | `NOT_SELECTED` | opt-in reference runtime |
| Background Sync guarantee | `PLATFORM_LIMITED` | wake-only enhancement |
---
## 2. 구현 결정 폐쇄 표
다음 표의 선택은 구현 중 다시 묻지 않는다.
| 질문 | 결정 |
| --- | --- |
| capability별 새 `src/platform/*`를 만드는가 | 아니다. 기존 `contracts/application/adapters/presentation/bootstrap/features`에 배치한다. |
| API·이벤트 schema를 frontend에 복사하는가 | 아니다. exact immutable contract package만 소비한다. |
| deployed OpenAPI/AsyncAPI를 runtime에 읽는가 | 아니다. build dependency만 사용한다. |
| `API_CONTRACT_VERSION` scalar를 유지하는가 | 제거한다. Release Manifest V2 `contractSet`으로 교체한다. |
| Runtime Config가 optional feature를 켤 수 있는가 | 아니다. static installed capability를 끌 수만 있다. |
| boot JSON을 `response.json()`으로 읽는가 | 아니다. streaming byte bound 후 UTF-8/JSON parse한다. |
| HTTP retry owner | HTTP transport 하나다. Query/Mutation retry는 false다. |
| redirect | 모든 API/boot request에서 reject한다. |
| mutation timeout/abort를 실패로 단정하는가 | 아니다. send 이후는 `MAYBE_APPLIED`다. |
| Router loader가 server fetch/cache를 소유하는가 | 아니다. URL parse/guard/lazy lifecycle만 소유한다. |
| QueryClient 수 | application runtime당 하나다. |
| Query persistence | 기본 비활성이다. Web Storage와 IndexedDB에 query payload를 넣지 않는다. |
| query result size 추정 | 모든 definition의 required `measureResult`만 사용한다. generic fallback은 없다. |
| cross-tab data 전송 | 금지한다. opaque invalidation topic만 전송한다. |
| realtime transport 자동 fallback | 금지한다. registration이 explicit fallback을 선택한다. |
| connection open과 resume success | 별도 상태다. `OPEN_UNRESUMED`에서 event를 적용하지 않는다. |
| event checkpoint commit | application effect 성공 이후다. |
| component가 raw socket을 소유하는가 | 아니다. ref-counted subscription lease만 소유한다. |
| generic Worker 기본 설치 | 아니다. profiling과 static contribution이 있어야 한다. |
| Worker가 network/auth를 소유하는가 | 아니다. CPU task만 수행한다. |
| PWA와 Web Push worker registration을 나누는가 | 아니다. 동일 scope에 physical worker 하나다. |
| `skipWaiting()` | client drain handshake 성공 후에만 호출한다. |
| `clients.claim()` | baseline에서 사용하지 않는다. |
| navigation offline shell | 제공하지 않는다. navigation은 network-only다. |
| API response Cache Storage | 금지한다. verified static asset만 cache한다. |
| Service Worker 등록 시점 | Runtime Config/release 검증과 첫 committed render 이후다. |
| Background Sync가 command를 전송하는가 | baseline에서는 아니다. foreground drain wake-up만 기록한다. |
| offline sender 수 | origin/account partition당 하나다. |
| `SENDING` 중 crash | `PENDING`으로 되돌리지 않고 `EFFECT_UNKNOWN`이다. |
| 일반 disabled capability | listener/DB/worker/timer를 만들지 않는다. |
| persisted Service Worker disable | 새 register 없이 exact-owned registration만 unregister하고 cache를 보존한다. |
| Service Worker source 제거 | `ACTIVE -> REMOVE_REGISTRATION -> PURGE_OWNED_RESOURCES -> null` 순서다. |
| lifecycle correctness를 unload에 의존하는가 | 아니다. transaction, TTL, recovery로 닫는다. |
---
## 3. 목표 런타임 구조와 lifecycle
### 3.1 전체 흐름
```text
Vite embedded build identity
+ installed contract contributions
+ installed runtime capabilities
|
v
bounded Runtime Config V2
|
v
bounded Release Manifest V2 + contractSet check
|
v
base composition
HTTP / auth bridge / application / QueryClient / Router
|
v
React first committed render
|
v
post-mount optional runtime start
offline foreground observer
realtime hosts
lazy Web Worker registry
Service Worker active/cleanup controller
```
### 3.2 boot state machine
```text
UNINITIALIZED
-> LOADING_RUNTIME_CONFIG
-> VALIDATING_RUNTIME_CONFIG
-> LOADING_RELEASE_MANIFEST
-> VALIDATING_RELEASE_COHERENCE
-> COMPOSING_BASE
-> MOUNTING
-> BASE_READY
-> STARTING_OPTIONAL
-> READY
```
terminal/degraded state:
```text
BOOT_FAILED_SAFE
BASE_READY_OPTIONAL_DEGRADED
DISPOSED
```
`BOOT_FAILED_SAFE`에서는 product Router와 Query provider를 mount하지 않는다. safe boot shell만 render한다.
### 3.3 composition result
```ts
export interface RuntimeComposition {
readonly application: ApplicationApi;
readonly queryClient: QueryClient;
readonly router: Router;
readonly scopeRuntime: ServerStateScopeRuntime;
readonly optional: OptionalRuntimeHost;
dispose(): Promise<void>;
}
```
feature page에 다음을 노출하지 않는다.
- raw Fetch client;
- endpoint URL;
- auth credential attacher;
- browser storage;
- QueryClient;
- WebSocket/EventSource;
- Worker;
- ServiceWorkerRegistration;
- IndexedDB transaction.
### 3.4 optional runtime host
```ts
export interface OptionalRuntimeHost {
readonly realtime: RealtimeRuntimeHost | null;
readonly webWorkers: WebWorkerRuntimeHost | null;
readonly serviceWorker: ServiceWorkerRuntimeHost | null;
readonly offlineCommands: OfflineCommandRuntimeHost | null;
startAfterMount(): Promise<void>;
stop(): Promise<void>;
}
```
start order:
```text
1. offline foreground browser lifecycle observer
2. realtime runtime
3. Web Worker prewarm 없음
4. Service Worker active/cleanup controller
```
stop order는 역순이다.
### 3.5 static capability selection SSOT
```text
src/features/installed-runtime-capabilities.ts
```
```ts
export interface InstalledRuntimeCapabilities {
readonly realtime: readonly InstalledRealtimeContribution[];
readonly webWorkers: readonly InstalledWebWorkerContribution[];
readonly serviceWorker: InstalledServiceWorkerSelection | null;
readonly offlineCommands: InstalledOfflineCommandContribution | null;
}
export type RuntimeCapabilityOverride = "DEFAULT" | "DISABLED";
export type InstalledServiceWorkerSelection =
| Readonly<{
mode: "ACTIVE";
scriptPath: "service-worker.js";
handlers: readonly ServiceWorkerHandlerId[];
}>
| Readonly<{
mode: "REMOVE_REGISTRATION";
scriptPath: "service-worker.js";
}>
| Readonly<{
mode: "PURGE_OWNED_RESOURCES";
scriptPath: "service-worker.js";
}>;
export type ServiceWorkerHandlerId =
| "WEB_PUSH"
| "PWA_STATIC_ASSETS"
| "OFFLINE_SYNC_WAKEUP";
```
규칙:
- source contribution이 설치 capability를 결정한다;
- Runtime Config는 `DEFAULT | DISABLED`만 제공한다;
- config가 source에 없는 capability를 켤 수 없다;
- dynamic import path를 config 문자열에서 만들지 않는다;
- contribution array와 object는 composition 시 deep snapshot/freeze한다;
- duplicate ID와 contradictory selection은 mount 전 실패한다.
### 3.6 zero-side-effect 원칙과 persistent 예외
realtime/Web Worker/offline command가 unselected 또는 disabled이면:
```text
factory call 0
window listener 0
timer 0
network 0
IndexedDB open 0
Worker creation 0
```
Service Worker는 browser에 persistent registration이 남을 수 있으므로 다음 예외만 허용한다.
```text
static ACTIVE + runtime DISABLED
-> new register 0
-> exact owned registration lookup 1
-> owned registration unregister at most 1
-> owned cache delete 0
```
이 예외를 다른 capability의 hidden cleanup side effect 근거로 확대하지 않는다.
---
## 4. 외부 계약 package 소비 경계
### 4.1 단일 import 위치
service별 generated package import는 다음 파일 한 곳에서만 허용한다.
```text
src/features/<feature>/contracts/<service>-contract-contribution.ts
```
예:
```text
src/features/worklog/
contracts/
worklog-service-contract-contribution.ts
worklog-mapper.ts
application/
adapters/
worklog-http-gateway.ts
presentation/
```
금지 import:
```text
src/domain/** -> @org-contracts/*
src/application/** -> @org-contracts/*
src/presentation/** -> @org-contracts/*
src/adapters/http/** -> service-specific package
src/bootstrap/** -> service-specific package direct import
```
bootstrap은 normalized feature contribution만 수집한다.
### 4.2 normalized contribution
```ts
export interface InstalledContractContribution {
readonly featureId: string;
readonly source: ContractContributionSource;
readonly http: readonly InstalledHttpContract<unknown, unknown, unknown>[];
readonly events: readonly InstalledEventContract<unknown, unknown>[];
}
export type ContractContributionSource =
| Readonly<{
kind: "EXTERNAL_PACKAGE";
package: InstalledContractPackageIdentity;
}>
| Readonly<{
kind: "TEMPLATE_FIXTURE";
fixtureId: "REFERENCE_FEATURE_V1";
revision: 1;
}>;
export interface InstalledContractPackageIdentity {
readonly packageId: string;
readonly version: string;
readonly digest: `sha256:${string}`;
readonly runtimeProtocolVersion: 1;
readonly sourceRevision: string;
}
```
frontend interface는 package의 business model을 소유하지 않는다. descriptor와 runtime validator reference만 보관한다.
### 4.3 HTTP contribution
```ts
export interface InstalledHttpContract<Input, WireOutput, Problem> {
readonly contract: Readonly<{
operationId: string;
method: "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE";
pathTemplate: string;
inputValidator: RuntimeValidator<Input>;
outputValidator: RuntimeValidator<WireOutput>;
problemValidator: RuntimeValidator<Problem>;
acceptedStatuses: readonly number[];
emptyBodyStatuses: readonly number[];
retrySemantics: "SAFE" | "IDEMPOTENT" | "KEYED" | "NEVER";
requestBody: "NONE" | "JSON";
responseBody: "REQUIRED_JSON" | "OPTIONAL_JSON" | "NONE";
commandRecovery: CommandRecoveryDescriptor | null;
commandEffect: CommandEffectDescriptor<Problem> | null;
}>;
readonly frontend: HttpExecutionPolicy;
}
export interface HttpExecutionPolicy {
readonly policyId: string;
readonly requestByteLimit: number;
readonly responseByteLimit: number;
readonly totalDeadlineMs: number;
readonly retryBudget: 0 | 1 | 2;
readonly authProfileId: string;
readonly diagnosticsOperation: string;
}
```
`contract`는 package가 제공한다. frontend가 method/path/status/retry/effect 의미를 다시 적지 않는다.
`frontend`만 이 저장소가 소유한다. 값은 §7의 hard ceiling 안에 있어야 한다.
### 4.4 command recovery descriptor
frontend는 server semantics를 새로 설계하지 않고 package가 제공하는 recovery capability만 소비한다.
```ts
export type CommandRecoveryDescriptor = Readonly<{
mode: "IDEMPOTENCY_REPLAY" | "INSPECT_OPERATION";
operationIdentityField: string;
inspectOperationId?: string;
}>;
export interface CommandEffectDescriptor<Problem> {
readonly successEffect: "APPLIED_CONFIRMED";
classifyProblem(input: Readonly<{
status: number;
problem: Problem;
}>): "NOT_APPLIED" | "APPLIED_CONFIRMED" | "MAYBE_APPLIED";
}
```
규칙:
- query/read operation은 `commandRecovery=null`, `commandEffect=null`이다;
- command operation은 `commandEffect`가 필수다;
- command operation에 `retrySemantics=KEYED`이면 `commandRecovery`도 필수다;
- descriptor가 없거나 invalid이면 automatic retry와 durable queue가 모두 금지된다;
- frontend는 inspect request/response wire shape나 problem effect meaning을 작성하지 않는다;
- `classifyProblem`은 package가 제공한 pure bounded classifier다;
- classifier가 throw/unknown을 반환하면 effect는 fail-safe `MAYBE_APPLIED`이고 contract runtime failure를 기록한다;
- feature gateway가 package의 inspect operation을 application-safe outcome으로 mapping한다.
### 4.5 event contribution
```ts
export interface InstalledEventContract<Envelope, Payload> {
readonly eventType: string;
readonly envelopeValidator: RuntimeValidator<Envelope>;
readonly payloadValidator: RuntimeValidator<Payload>;
}
```
package identity는 enclosing `InstalledContractContribution.source`가 단 한 번 소유한다. HTTP와 event가 서로 다른 package identity라면 contribution을 둘로 나눈다.
connection, queue, dedupe, scope, recovery와 application effect는 frontend realtime contribution이 소유한다. generated package는 socket을 열거나 QueryClient를 참조하지 않는다.
### 4.6 runtime validator protocol
```ts
export interface RuntimeValidator<T> {
readonly schemaId: string;
safeParse(value: unknown):
| Readonly<{ success: true; data: T }>
| Readonly<{
success: false;
issues: readonly RuntimeValidationIssue[];
}>;
}
export interface RuntimeValidationIssue {
readonly path: readonly (string | number)[];
readonly code: string;
}
```
규칙:
- external response/event는 항상 `unknown`에서 시작한다;
- generated interface와 `as T`는 validator를 대체하지 않는다;
- validator throw는 `CONTRACT_RUNTIME_FAILURE`다;
- ordinary mismatch는 `CONTRACT_VALUE_INVALID`다;
- raw value, server detail, validator internals를 diagnostics에 넣지 않는다;
- 성공 값은 mapper 전에 caller-owned mutable reference로 재사용하지 않는다.
### 4.7 mapper ownership
```text
wire DTO
-> feature contract mapper
-> domain factory/application value
-> Query cache/UI
```
mapper MUST:
- feature가 소유한다;
- no-throw discriminated result를 반환한다;
- date/number/enum/null을 application canonical value로 변환한다;
- generated DTO를 Query cache에 직접 넣지 않는다;
- package metadata/header를 application value에 포함하지 않는다;
- collection count와 field bound를 mapper 단계에서도 확인한다.
```ts
export type MappingResult<T> =
| Readonly<{ ok: true; value: T }>
| Readonly<{
ok: false;
error: Readonly<{ kind: "MAPPING_CONTRACT_VIOLATION"; code: string }>;
}>;
```
### 4.8 installed contribution composer
```text
src/features/installed-contract-contributions.ts
```
```ts
export const INSTALLED_CONTRACT_CONTRIBUTIONS = Object.freeze([
REFERENCE_FEATURE_TEMPLATE_CONTRIBUTION,
] as const);
```
reference fixture 규칙:
```text
featureId = reference-feature
source.kind = TEMPLATE_FIXTURE
fixtureId = REFERENCE_FEATURE_V1
revision = 1
events = []
```
`TEMPLATE_FIXTURE`는 current reference HTTP vertical을 deterministic test data로 유지하기 위한 유일한 예외다. product feature는 반드시 `EXTERNAL_PACKAGE`다. fixture는 `contractSet`에 포함하지 않는다.
### 4.9 composer exact validation
composition 전에 다음을 모두 검사한다.
```text
featureId:
^[a-z][a-z0-9-]{0,63}$
packageId:
^@[a-z0-9][a-z0-9._-]{0,62}/[a-z0-9][a-z0-9._-]{0,62}$
version:
exact SemVer
leading v 금지
range/tag/whitespace 금지
digest:
^sha256:[0-9a-f]{64}$
sourceRevision:
^[0-9a-f]{7,64}$
operationId:
^[A-Za-z][A-Za-z0-9_.-]{0,127}$
eventType:
UTF-8 <= 160 bytes
whitespace/control character 금지
pathTemplate:
startsWith /
UTF-8 <= 512 bytes
query/hash 금지
```
추가 검증:
- external runtime protocol version은 정확히 1;
- package ID별 정확히 한 version/digest;
- duplicate operation ID 없음;
- duplicate event type 없음;
- status는 100..599, 1..32개, sorted unique;
- empty-body status는 accepted status의 subset;
- validator와 non-empty `schemaId` 필수;
- request/response limit은 §7 hard ceiling 이하;
- same operation method/path/status projection drift 없음;
- fixture 외 local product contract 없음.
package identity 검증은 `EXTERNAL_PACKAGE`에만 적용한다. fixture에 가짜 digest를 부여하지 않는다.
### 4.10 package dependency 규칙
- `package.json`은 exact version만 사용한다;
- `latest`, caret, tilde, tag, Git URL, workspace link 금지;
- lockfile resolved integrity와 package export manifest가 존재해야 한다;
- runtime registry/Git/deployed docs에서 package를 보충하지 않는다;
- package root barrel 전체 import 금지;
- 사용 operation/event subpath만 정적 import한다;
- package import는 top-level network/DOM/listener side effect가 없어야 한다;
- package source와 generated output을 frontend에서 수정하지 않는다.
### 4.11 reference feature에서 product feature로의 migration
금지:
```text
copy reference-schemas.ts
edit URL/status/type
ship as product local contract
```
필수:
```text
external package pin
-> feature contribution adapter
-> feature mapper
-> feature gateway
-> application input
-> query/mutation/realtime bridge
```
reference fixture는 template 자체 제거 가능성 검증용이며 서비스 계약 authoring template가 아니다.
---
## 5. Contract Set과 release coherence
### 5.1 scalar 계약 version 제거
현재 다음 scalar는 여러 package를 표현할 수 없다.
```text
RuntimeConfig.API_CONTRACT_VERSION
ReleaseManifest.apiContractVersion
```
V2 전환 결과:
```text
Runtime Config
= deployment/browser operational setting
Release Manifest
= build/release/assets/compiled external contract package set
```
`RuntimeConfig`에서 API contract version을 제거한다.
### 5.2 Release Manifest V2
```ts
export interface ReleaseManifestV2 {
readonly schemaVersion: 2;
readonly appVersion: string;
readonly buildId: string;
readonly commitSha: string;
readonly configSchemaVersion: string;
readonly assetManifestHash: string;
readonly releaseId: string;
readonly builtAt: string;
readonly routeChunks: Readonly<Record<string, string>>;
readonly contractSet: Readonly<{
setAlgorithm: "CA_CONTRACT_SET_V1";
setDigest: `sha256:${string}`;
packages: readonly ContractSetPackage[];
}>;
}
export interface ContractSetPackage {
readonly packageId: string;
readonly version: string;
readonly digest: `sha256:${string}`;
readonly runtimeProtocolVersion: 1;
readonly sourceRevision: string;
}
```
fixture contribution은 `packages`에 들어가지 않는다.
### 5.3 canonicalization
단일 구현:
```text
src/contracts/contract-set-canonical.ts
```
```ts
export function canonicalizeContractSet(
packages: readonly ContractSetPackage[],
): Uint8Array;
```
algorithm:
```text
header = UTF-8("CA_FRONTEND_CONTRACT_SET_V1\0")
entries = packages sorted by UTF-8 byte lexicographic packageId
for each entry:
append u32be(length(packageId)) + packageId bytes
append u32be(length(version)) + version bytes
append u32be(length(digest)) + digest bytes
append u32be(runtimeProtocolVersion)
append u32be(length(sourceRevision)) + sourceRevision bytes
setDigest = lower-hex SHA-256(canonical bytes)
```
중복 package ID는 hash 전에 실패한다. JSON property order나 locale sort에 의존하지 않는다.
Node build script와 browser runtime은 canonical byte 생성 함수를 공유한다. hash adapter만 Node `crypto`와 Web Crypto로 분리한다.
### 5.4 build-time expected set
```text
INSTALLED_CONTRACT_CONTRIBUTIONS
-> EXTERNAL_PACKAGE identities only
-> canonicalize
-> setDigest
-> build manifest embed
```
`TEMPLATE_FIXTURE`는 filtering 후 set에 포함하지 않는다.
### 5.5 runtime check
boot에서 다음을 비교한다.
```text
embedded expected package list
manifest package list
embedded expected setDigest
manifest setDigest
recomputed manifest setDigest
runtime protocol version
```
모두 일치해야 한다. package range negotiation, deployed server docs와 `latest` lookup을 하지 않는다.
### 5.6 failure code
```text
CONTRACT_SET_SCHEMA_INVALID
CONTRACT_SET_ENTRY_INVALID
CONTRACT_SET_DUPLICATE_PACKAGE
CONTRACT_SET_DIGEST_INVALID
CONTRACT_SET_DIGEST_MISMATCH
CONTRACT_SET_PACKAGE_MISSING
CONTRACT_SET_PACKAGE_UNEXPECTED
CONTRACT_SET_VERSION_MISMATCH
CONTRACT_RUNTIME_PROTOCOL_UNSUPPORTED
```
이 failure는 product route mount 전 safe boot error다.
### 5.7 independent deployment 의미
frontend manifest는 **frontend build에 compile된 package set**을 검증한다. provider runtime version을 추측하거나 provider와 exact equality를 요구하지 않는다.
provider compatibility와 server support range는 별도 계약 시스템의 release responsibility다. frontend runtime은 build package set과 자신의 artifact coherence만 검증한다.
### 5.8 V1 migration
```text
Release 1
reader: V1 + V2
writer: V1
API_CONTRACT_VERSION 유지
Release 2
reader: V1 + V2
writer: V2
contractSet 생성
V1 input parser만 scalar를 읽음
V2 input에 scalar가 있으면 strict reject
Release 3
reader: V2
writer: V2
RuntimeConfig scalar reject as unknown field
V1 fixtures 제거
```
V1과 V2 값을 동시에 비교해 precedence를 만들지 않는다. V2 manifest에서 scalar가 추가로 존재하면 strict parser가 거절한다.
---
## 6. Runtime Config와 boot
### 6.1 Runtime Config V2
```ts
export const runtimeConfigV2Schema = z
.object({
APP_ENV: z.enum(["local", "development", "staging", "production"]),
API_BASE_URL: z.url(),
REQUEST_TIMEOUT_MS: z.int().min(100).max(60_000).default(10_000),
MAX_RETRY_ATTEMPTS: z.int().min(0).max(2).default(2),
TELEMETRY_ENABLED: z.boolean(),
TELEMETRY_ENDPOINT: z.url().optional(),
AUTH_MODE: z.enum(["external", "demo"]),
CONFIG_SCHEMA_VERSION: z.literal("2.0"),
RELEASE_MANIFEST_URL: z.string().min(1).default("/release-manifest.json"),
RELEASE_ID: z.string().min(1).optional(),
BUILD_ID: z.string().min(1).optional(),
CAPABILITY_OVERRIDES: z
.object({
REALTIME: z.enum(["DEFAULT", "DISABLED"]).default("DEFAULT"),
WEB_WORKER: z.enum(["DEFAULT", "DISABLED"]).default("DEFAULT"),
SERVICE_WORKER: z.enum(["DEFAULT", "DISABLED"]).default("DEFAULT"),
OFFLINE_COMMANDS: z.enum(["DEFAULT", "DISABLED"]).default("DEFAULT"),
})
.strict()
.default({}),
})
.strict();
```
`API_CONTRACT_VERSION`은 V2에 없다.
### 6.2 URL validation
`API_BASE_URL`:
- absolute URL;
- username/password/hash 없음;
- pathname은 `/`로 끝남;
- local/development 외 `https:`;
- query는 없음;
- canonical `new URL(value).href`를 runtime snapshot에 저장.
`TELEMETRY_ENDPOINT`:
- enabled일 때 필수;
- absolute URL;
- username/password/hash 없음;
- local/development 외 `https:`;
- API base와 같을 필요 없음.
`RELEASE_MANIFEST_URL`:
- same-origin absolute path;
- `/`로 시작;
- query/hash 없음;
- `//`, backslash, percent-encoded slash, dot segment 거절;
- maximum UTF-8 256 bytes.
### 6.3 config secret-name scan
기존 `assertSafeConfigNames`를 유지하고 다음 key fragment를 case-insensitive로 거절한다.
```text
PASSWORD
SECRET
TOKEN
PRIVATE_KEY
CLIENT_SECRET
ACCESS_KEY
REFRESH_TOKEN
COOKIE
AUTHORIZATION
```
`AUTH_MODE`, `TELEMETRY_ENABLED`처럼 fragment가 semantic enum인 known allowlist는 exact key allowlist로만 허용한다. arbitrary nested key에는 allowlist를 적용하지 않는다.
### 6.4 bounded boot body reader
공통 source:
```text
src/bootstrap/read-bounded-boot-json.ts
```
```ts
export interface BootJsonPolicy {
readonly operation: "RUNTIME_CONFIG" | "RELEASE_MANIFEST";
readonly maximumBytes: number;
readonly totalDeadlineMs: 5_000;
}
```
numeric policy:
```text
Runtime Config maximum = 65,536 bytes
Release Manifest maximum = 1,048,576 bytes
total deadline = 5,000 ms each
```
request options:
```ts
{
method: "GET",
cache: "no-store",
credentials: "same-origin",
redirect: "error",
referrerPolicy: "no-referrer",
headers: { Accept: "application/json" },
signal,
}
```
### 6.5 response admission
순서:
```text
fetch settled
-> status 200 only
-> content-type parse
-> application/json or +json only
-> Content-Length precheck when finite
-> ReadableStream bounded read
-> actual bytes <= policy maximum
-> UTF-8 fatal decode
-> JSON.parse
-> object shape check
-> Zod strict parse
-> deep snapshot
```
금지:
- `response.json()`;
- unbounded `response.text()`;
- redirect follow;
- HTML error page를 JSON으로 parse;
- `Content-Length`만 신뢰;
- timeout 후 body read continuation.
body overflow 시 reader를 cancel하고 `AbortController`를 abort한다.
### 6.6 boot failure union
```ts
export type BootLoadFailure =
| "FETCH_FAILED"
| "TIMEOUT"
| "HTTP_STATUS_INVALID"
| "CONTENT_TYPE_INVALID"
| "BODY_TOO_LARGE"
| "UTF8_INVALID"
| "JSON_INVALID"
| "SHAPE_INVALID"
| "SECRET_NAME_REJECTED"
| "SCHEMA_INVALID"
| "BUILD_MISMATCH"
| "RELEASE_MISMATCH"
| "ASSET_MISMATCH"
| "CONTRACT_SET_MISMATCH";
```
safe error는 다음만 포함한다.
```text
failure kind/code
buildId
releaseId if already validated
config schema version if safe
support reference = buildId + bounded failure code
```
URL, response body, validation value, endpoint hostname, stack trace를 포함하지 않는다.
### 6.7 boot order
```text
1. read embedded build config
2. load/validate Runtime Config V2
3. compare optional BUILD_ID
4. load/validate Release Manifest V2
5. verify build/config/release/assets
6. verify contractSet
7. compile static optional selection + runtime overrides
8. create diagnostics/telemetry no-op or selected sink
9. create auth/session adapter
10. create HTTP runtime
11. create application inputs/gateways
12. create QueryClient/scope/invalidation
13. create Router
14. create optional host objects without start side effect
15. mount React tree
16. committed effect starts optional runtime
```
### 6.8 partial composition rollback
각 step은 cleanup stack에 disposer를 push한다.
```text
create A -> push dispose A
create B -> push dispose B
create C fails
-> dispose B
-> dispose A
-> render safe boot error
```
cleanup은 reverse order이며 하나의 cleanup failure가 다음 cleanup을 막지 않는다. 모든 cleanup failure를 bounded count와 kind로 합산한다.
### 6.9 boot retry
safe boot shell의 retry action:
- 사용자의 explicit action만;
- 동일 page session에서 maximum 1회;
- 이전 attempt의 controller/resource settle 후 실행;
- 1초 minimum interval;
- retry 실패 후 automatic loop 없음;
- retry success 시 full product root를 새로 mount;
- partial composition object 재사용 금지.
### 6.10 Runtime Config immutability
validated config는 deep snapshot/freeze한다.
- runtime 중 config endpoint polling 없음;
- dynamic mutation 없음;
- kill switch 변경은 page reload/new boot에서만 적용;
- config object를 context로 노출하지 않음;
- application은 typed port를 통해 필요한 value만 본다.
---
## 7. Fetch HTTP execution runtime
### 7.1 source ownership
유지/수정:
```text
src/adapters/http/client.ts
src/adapters/http/request-builder.ts
src/adapters/http/bounded-json.ts
src/adapters/http/retry-policy.ts
```
신규:
```text
src/adapters/http/bounded-body-reader.ts
src/adapters/http/http-execution-v3.ts
src/adapters/http/http-effect-certainty.ts
src/adapters/http/http-contract-bridge.ts
```
범용 Axios-like API를 만들지 않는다. caller는 `InstalledHttpContract`와 typed input을 전달한다.
### 7.2 execution interface
```ts
export interface ContractHttpExecutor {
execute<Input, WireOutput, Problem>(
operation: InstalledHttpContract<Input, WireOutput, Problem>,
input: Input,
context: HttpExecutionContext,
): Promise<HttpExecutionOutcome<WireOutput, Problem>>;
}
export interface HttpExecutionContext {
readonly signal?: AbortSignal;
readonly scope: CacheScopeSnapshot;
readonly intent?: MutationIntentContext;
}
```
`MutationIntentContext`는 command에만 사용한다.
```ts
export interface MutationIntentContext {
readonly intentId: string;
readonly idempotencyKey?: string;
readonly startedBy: "USER" | "FOREGROUND_RETRY" | "OUTBOX_REPLAY";
}
```
### 7.3 hard ceilings
```text
default request body 262,144 bytes
hard request body 1,048,576 bytes
default success response 1,048,576 bytes
hard response body 8,388,608 bytes
problem response maximum 65,536 bytes
path template maximum 512 bytes
encoded query maximum 8,192 bytes
header value examined maximum 8,192 bytes per allowlisted header
total deadline default 10,000 ms
total deadline hard 60,000 ms
retry count hard 2
```
external contribution의 frontend policy는 default보다 작거나 같게 줄일 수 있다. hard ceiling을 초과할 수 없다.
### 7.4 input validation과 request projection
순서:
```text
capture scope
-> verify scope current
-> package input validator
-> feature/application input already canonical
-> request projection
-> encoded path/query/body byte check
-> credential patch
-> final invariant recheck
-> dispatch
```
final invariant recheck 항목:
- method unchanged;
- URL origin/path allowlist;
- redirect mode error;
- credentials mode expected;
- credential patch header allowlist;
- forbidden header 없음;
- body/request byte bound;
- abort/deadline not expired;
- scope still current.
final invariant failure면 fetch 호출은 0회다.
### 7.5 URL construction
- base는 validated `API_BASE_URL`;
- path placeholder는 descriptor의 named input projection만 사용;
- segment는 `encodeURIComponent` semantics로 한 번만 encode;
- raw slash를 path value에 허용하지 않음;
- query key order는 descriptor-generated order;
- array encoding은 package descriptor가 제공한 exact projection;
- `undefined` omit, `null`은 descriptor가 허용한 representation만;
- URL string concatenation 금지;
- absolute path override, origin override 금지;
- final URL maximum 16,384 bytes.
### 7.6 request body
`requestBody=NONE`이면 body와 Content-Type 모두 없다.
`requestBody=JSON`이면:
```text
package request projection
-> deterministic JSON encoder
-> UTF-8 bytes
-> requestByteLimit check
-> Content-Type application/json
```
`JSON.stringify`의 default property enumeration에 semantic digest를 의존하지 않는다. idempotency request digest가 필요하면 package가 canonical request digest projection을 제공한다.
### 7.7 credential bridge
```ts
export type CredentialPatchOutcome =
| Readonly<{
kind: "READY";
headers: Readonly<Record<string, string>>;
credentials: RequestCredentials;
}>
| Readonly<{ kind: "UNAUTHENTICATED" }>
| Readonly<{ kind: "UNAVAILABLE" }>
| Readonly<{ kind: "SCOPE_FENCED" }>;
```
허용 header:
```text
Authorization
X-CSRF-Token
X-Tenant-Context
```
실제 profile이 쓰는 subset만 등록한다. caller가 arbitrary header를 추가하지 않는다.
credential unavailable에서 anonymous request로 downgrade하지 않는다.
### 7.8 physical attempt state
```ts
export type PhysicalAttemptState =
| "PREPARING"
| "READY_TO_SEND"
| "DISPATCHED"
| "RESPONSE_HEADERS"
| "READING_BODY"
| "VALIDATING"
| "MAPPING_READY"
| "SETTLED";
```
`fetch()` invocation 직전에 `DISPATCHED`로 바꾸지 않는다. invocation expression에 들어가기 직전 local state를 `READY_TO_SEND`, promise가 반환된 직후 `DISPATCHED`로 기록한다. synchronous throw는 `NOT_STARTED`다.
### 7.9 response admission
순서:
```text
redirected=false
-> status package allowlist
-> content-type policy
-> Content-Length precheck
-> bounded stream read
-> empty-body policy
-> UTF-8/JSON parse when required
-> problem or success validator
-> scope current fence
-> typed outcome
```
`response.ok`만 사용하지 않는다.
### 7.10 body policy
`REQUIRED_JSON`:
- allowed success status에서 non-empty body 필수;
- JSON media 필수;
- empty/whitespace는 contract violation.
`OPTIONAL_JSON`:
- empty-body status 또는 zero body 허용;
- body가 있으면 JSON media/validator 필수;
- body 없는 값을 `undefined`로 명시적으로 mapping.
`NONE`:
- body를 application value로 읽지 않음;
- `Content-Length > 0`이면 즉시 violation + body cancel;
- length가 없고 body stream이 있으면 최대 1 byte만 probe한다;
- 첫 chunk가 있으면 violation + reader cancel;
- immediately done이면 empty success;
- descriptor가 unexpected body를 허용하지 않으므로 arbitrary drain하지 않는다.
### 7.11 typed outcome
```ts
export type HttpExecutionOutcome<Value, Problem> =
| Readonly<{
kind: "SUCCESS";
value: Value;
metadata: SafeResponseMetadata;
effect: "NOT_APPLICABLE" | "APPLIED_CONFIRMED";
}>
| Readonly<{
kind: "PROBLEM";
problem: Problem;
metadata: SafeResponseMetadata;
effect: "NOT_APPLIED" | "APPLIED_CONFIRMED" | "MAYBE_APPLIED";
}>
| Readonly<{
kind: "UNAUTHENTICATED";
effect: "NOT_APPLICABLE" | "NOT_APPLIED" | "MAYBE_APPLIED";
}>
| Readonly<{
kind: "FORBIDDEN";
effect: "NOT_APPLICABLE" | "NOT_APPLIED" | "MAYBE_APPLIED";
}>
| Readonly<{
kind: "RATE_LIMITED";
retryAfterMs?: number;
effect: "NOT_APPLICABLE" | "NOT_APPLIED" | "MAYBE_APPLIED";
}>
| Readonly<{
kind: "CONTRACT_VIOLATION";
violation: HttpContractViolation;
}>
| Readonly<{
kind: "TRANSPORT_FAILURE";
failure: HttpTransportFailure;
effect: "NOT_STARTED" | "MAYBE_APPLIED";
}>
| Readonly<{
kind: "CANCELLED";
effect: "NOT_STARTED" | "MAYBE_APPLIED";
}>;
```
### 7.12 safe response metadata
```ts
export interface SafeResponseMetadata {
readonly status: number;
readonly correlationReference?: string;
readonly retryAfterMs?: number;
readonly validatorToken?: string;
}
```
metadata에 raw header map, URL, server, cookie, traceparent, ETag raw value를 노출하지 않는다. validator token은 conditional validator store 내부 opaque binding이며 UI/application에 직접 전달하지 않는다.
### 7.13 contract violation
```text
UNEXPECTED_STATUS
UNEXPECTED_EMPTY_BODY
UNEXPECTED_BODY
CONTENT_TYPE_MISMATCH
RESPONSE_TOO_LARGE
UTF8_INVALID
JSON_INVALID
SUCCESS_SCHEMA_INVALID
PROBLEM_SCHEMA_INVALID
VALIDATOR_RUNTIME_FAILURE
MAPPING_CONTRACT_VIOLATION
FINAL_REQUEST_INVARIANT_FAILED
SCOPE_FENCED
```
```ts
export type HttpContractViolation = Readonly<{
kind:
| "UNEXPECTED_STATUS"
| "UNEXPECTED_EMPTY_BODY"
| "UNEXPECTED_BODY"
| "CONTENT_TYPE_MISMATCH"
| "RESPONSE_TOO_LARGE"
| "UTF8_INVALID"
| "JSON_INVALID"
| "SUCCESS_SCHEMA_INVALID"
| "PROBLEM_SCHEMA_INVALID"
| "VALIDATOR_RUNTIME_FAILURE"
| "MAPPING_CONTRACT_VIOLATION"
| "FINAL_REQUEST_INVARIANT_FAILED"
| "SCOPE_FENCED";
operation: "REQUEST" | "RESPONSE" | "VALIDATION" | "MAPPING";
}>;
```
contract violation은 ordinary network retry 대상이 아니다.
### 7.14 transport failure
```text
DNS_OR_CONNECT_FAILURE
TLS_OR_NETWORK_FAILURE
TIMEOUT
RESPONSE_STREAM_FAILURE
ABORTED_BY_SCOPE
ABORTED_BY_CALLER
OVERLOADED_BEFORE_SEND
```
```ts
export type HttpTransportFailure = Readonly<{
kind:
| "DNS_OR_CONNECT_FAILURE"
| "TLS_OR_NETWORK_FAILURE"
| "NETWORK_FAILURE"
| "TIMEOUT"
| "RESPONSE_STREAM_FAILURE"
| "ABORTED_BY_SCOPE"
| "ABORTED_BY_CALLER"
| "OVERLOADED_BEFORE_SEND";
retryable: boolean;
}>;
```
browser가 세부 네트워크 원인을 제공하지 않으면 하나의 `NETWORK_FAILURE` bucket으로 축약한다. native exception message를 boundary 밖으로 내보내지 않는다.
---
## 8. Retry·cancellation·mutation effect certainty
### 8.1 retry owner
```text
HTTP transport retry = enabled by exact operation policy
TanStack Query retry = false
TanStack Mutation retry = false
realtime reconnect = realtime coordinator
Background Sync retry = offline drain state machine
```
서로 다른 계층이 같은 physical operation을 중복 retry하지 않는다.
### 8.2 HTTP retry algorithm
```text
base delay = 250 ms
max local delay = 2,000 ms
algorithm = full jitter
maximum retries = min(operation.retryBudget, RuntimeConfig.MAX_RETRY_ATTEMPTS, 2)
```
retry index 0의 ceiling:
```text
min(2,000, 250 * 2^0) = 250 ms
```
실제 delay:
```text
floor(random[0,1) * ceiling)
```
validated `Retry-After`가 있으면 local delay의 lower bound로 사용한다. 5,000ms를 넘거나 remaining total deadline을 넘으면 retry하지 않는다.
### 8.3 retry classification
`SAFE`:
- GET/HEAD;
- network failure after dispatch 허용;
- 408, 425, 429, 502, 503, 504 중 accepted retry policy;
- same frozen request 사용.
`IDEMPOTENT`:
- package가 operation 자체 idempotent라고 선언;
- same frozen request;
- SAFE와 같은 failure/status subset.
`KEYED`:
- same user intent의 동일 idempotency key 필수;
- known response status가 retry 가능하다고 package가 명시한 경우만;
- dispatch 후 response 없는 network/timeout/abort는 automatic retry 금지;
- inspect/reconciliation path로 이동.
`NEVER`:
- automatic retry 0.
### 8.4 auth retry
401 response 뒤 hidden refresh-and-replay를 하지 않는다.
- credential owner는 **send 전** READY/UNAUTHENTICATED/UNAVAILABLE을 결정한다;
- response 401은 terminal `UNAUTHENTICATED`;
- 사용자 re-auth 뒤 새 user intent 또는 explicit retry로 실행;
- command를 새 credential로 자동 replay하지 않는다.
### 8.5 deadline
one logical operation deadline은 다음 전체를 포함한다.
```text
credential resolution
request encode
retry backoff
all physical attempts
response read
runtime validation
mapper preparation
```
operation start에서 monotonic deadline을 capture한다. 각 sub-step이 remaining budget을 받는다. physical attempt별로 full timeout을 새로 부여하지 않는다.
### 8.6 cancellation sources
```ts
export type CancellationOwner =
| "CALLER"
| "ROUTE_TRANSITION"
| "SCOPE_FENCE"
| "APPLICATION_SHUTDOWN"
| "DEADLINE";
```
linked AbortController는 first reason만 terminal reason으로 저장한다. later reason은 diagnostics count에만 반영한다.
### 8.7 mutation effect certainty
```ts
export type MutationEffectCertainty =
| "NOT_STARTED"
| "NOT_APPLIED"
| "MAYBE_APPLIED"
| "APPLIED_CONFIRMED";
```
판정:
| 관측 | certainty |
| --- | --- |
| validation/config/auth/final invariant에서 종료 | `NOT_STARTED` |
| fetch synchronous throw | `NOT_STARTED` |
| package가 no-effect problem/status로 명시 | `NOT_APPLIED` |
| success/committed receipt | `APPLIED_CONFIRMED` |
| dispatch 후 timeout/network/abort/body loss | `MAYBE_APPLIED` |
| unknown/unclassified problem | `MAYBE_APPLIED` |
frontend가 HTTP status만 보고 `NOT_APPLIED`를 추측하지 않는다.
### 8.8 mutation intent identity
```ts
export interface MutationIntent {
readonly intentId: string;
readonly operationId: string;
readonly canonicalInputIdentity: string;
readonly idempotencyKey?: string;
readonly createdAtMonotonicMs: number;
}
```
- `intentId`: `crypto.randomUUID()`;
- 같은 submit의 transport retry/reconcile에는 동일 intent/key;
- form value가 변경되면 새 intent;
- double click은 duplicate coordinator가 처리;
- key/identity를 log/URL/query key에 넣지 않는다.
### 8.9 uncertain outcome handling
`MAYBE_APPLIED`면:
```text
1. automatic resend 금지
2. optimistic layer를 arbitrary rollback하지 않음
3. mutation coordinator state = EFFECT_UNKNOWN
4. package recovery descriptor 확인
5. inspect operation이 있으면 bounded inspect
6. committed -> canonical result 적용
7. no effect -> explicit user retry 허용
8. unknown 유지 -> status UI/support path
```
inspect도 timeout이면 unknown을 유지한다.
### 8.10 UI projection
```text
NOT_STARTED -> ordinary retry 가능
NOT_APPLIED -> policy상 retry 가능
MAYBE_APPLIED -> "처리 상태 확인 필요"
APPLIED_CONFIRMED-> success
```
사용자 문구는 i18n catalog가 소유한다. native error와 server detail을 표시하지 않는다.
---
## 9. Router
### 9.1 ownership
Router owns:
```text
route registry
path/search codec
navigation
redirect
lazy module lifecycle
route error boundary
focus/scroll/title
dirty navigation guard
session/access hint guard
```
Router does not own:
```text
server data cache
HTTP retry
mutation lifecycle
realtime connection
business authorization
contract package loading
```
### 9.2 server-data loader/action 금지
baseline에서 loader/action은 server fetch를 수행하지 않는다.
허용 loader:
- URL/path/search parse;
- local route registration lookup;
- synchronous redirect;
- session snapshot 기반 access hint;
- route-local static metadata.
금지:
- direct fetch;
- gateway call;
- QueryClient prefetch/ensure;
- IndexedDB read;
- mutation action;
- separate loader cache.
서버 data는 page의 feature query/mutation hook이 application input을 호출해 가져온다.
### 9.3 route registration
```ts
export interface InstalledRouteContribution {
readonly routeId: string;
readonly path: string;
readonly ownerFeatureId: string;
readonly load: () => Promise<RouteRuntimeModule>;
readonly inputCodecId: string;
readonly accessPolicyId: string;
}
```
- route ID: existing registry format 유지;
- duplicate route ID/path 거절;
- feature page가 app-router를 import하지 않음;
- lazy module은 route runtime module만 export;
- Router가 feature gateway/adapter를 props로 전달하지 않음.
### 9.4 URL codec
path/search raw input은 `unknown`이다.
```text
URLSearchParams
-> registered route codec
-> normalized route input
-> RouteInputProvider
-> page/controller
```
- `Object.fromEntries(searchParams)` raw trust 금지;
- duplicate query key policy를 codec이 명시;
- unknown key는 default reject;
- array/count/string byte bound;
- route param은 domain ID factory 전까지 string;
- malformed input은 safe bad-request route surface;
- network/application gateway call 0.
### 9.5 route transition cleanup
feature page unmount 순서:
```text
1. Query observer release
2. feature realtime subscription lease release
3. page browser listener cleanup
4. form/dirty guard cleanup
5. object URL/temporary file lease cleanup
6. page-owned abort controller abort
```
다음 global runtime은 유지한다.
```text
QueryClient
scope runtime
cross-context coordinator
realtime host
Service Worker registration
offline repository
```
### 9.6 chunk recovery
```text
chunk load failure
-> no-store Release Manifest V2 refresh
-> build/release/asset/contractSet compare
-> dirty state inspect
-> one guarded reload
-> second failure terminal safe surface
```
reload guard key:
```text
CHUNK_RELOAD_GUARD = oldBuildId + newBuildId + failedRouteId
```
동일 pair/route당 한 번만 reload한다. localStorage record는 10분 TTL과 512-byte hard cap을 갖는다.
### 9.7 focus/scroll/title
- successful route commit 후 `main-content` focus;
- navigation마다 scroll top;
- explicit registered hash-anchor route만 anchor focus;
- document title은 typed message catalog;
- background Query refresh는 focus/scroll을 변경하지 않음;
- reduced motion 존중;
- error route도 heading focus를 제공.
### 9.8 navigation blocker
blocking 조건:
```text
dirty form
active non-cancellable local file workflow
mutation EFFECT_UNKNOWN recovery in progress
offline command conflict resolution draft
```
ordinary loading/query refresh는 navigation을 막지 않는다.
browser unload dialog는 browser standard prompt만 사용한다. custom string에 의존하지 않는다.
---
## 10. Query lifecycle과 cache
### 10.1 QueryClient 수와 기본 정책
application runtime당 QueryClient 하나다.
```ts
export const QUERY_CACHE_DEFAULTS = Object.freeze({
staleTime: 30_000,
gcTime: 300_000,
refetchOnWindowFocus: true,
retry: false,
mutationRetry: false,
persistence: false,
});
```
route/feature/session마다 새 QueryClient를 만들지 않는다.
### 10.2 fixed server-state profiles
```ts
export type ServerStateProfileId =
| "DETAIL_STANDARD"
| "LIST_STANDARD"
| "LOOKUP_STABLE"
| "VOLATILE_STATUS";
export interface ServerStateProfile {
readonly profileId: ServerStateProfileId;
readonly staleTimeMs: number;
readonly gcTimeMs: number;
readonly refetchOnWindowFocus: boolean;
readonly maxResultItems: number;
readonly maxEstimatedResultBytes: number;
}
```
```text
DETAIL_STANDARD
stale 30s, gc 5m, focus true, items 1, bytes 256KiB
LIST_STANDARD
stale 15s, gc 5m, focus true, items 200, bytes 1MiB
LOOKUP_STABLE
stale 5m, gc 30m, focus false, items 500, bytes 2MiB
VOLATILE_STATUS
stale 0, gc 1m, focus true, items 1, bytes 64KiB
```
feature가 임의 숫자를 선언하지 않는다. 네 profile로 표현할 수 없으면 본 문서와 profile registry를 개정한다.
### 10.3 query definition
```ts
export interface QueryDefinition<Input, Value> {
readonly definitionId: string;
readonly definitionVersion: number;
readonly owner: string;
readonly namespace: string;
readonly namespaceVersion: number;
readonly operationId: string;
readonly profileId: ServerStateProfileId;
measureResult(value: Value): QueryResultMeasure;
execute(
input: Input,
context: Readonly<{ signal: AbortSignal }>,
): Promise<Result<Value>>;
}
export interface QueryResultMeasure {
readonly itemCount: number;
readonly estimatedBytes: number;
}
```
### 10.4 result measurement
`measureResult`는 모든 definition에 필수다.
- detail/scalar: item 1;
- empty optional: item 0;
- list/page: actual mapped items;
- bytes: mapped application value의 bounded string UTF-8 bytes + primitive fixed width + collection overhead;
- `JSON.stringify` fallback 금지;
- wire DTO 재직렬화 금지;
- generic recursive walker 금지;
- throw/non-finite/negative/unsafe integer는 `RESULT_MEASUREMENT_FAILED`;
- ceiling 초과는 `RESULT_BUDGET_EXCEEDED`;
- measurement failure/overflow는 cache commit 0, retry 0.
### 10.5 client scope authority
현재 `ServerStateScopeRuntime`을 browser client scope authority로 확장한다.
```ts
export type ClientScopeLifecycleEvent =
| Readonly<{ kind: "FENCED"; previousGeneration: number }>
| Readonly<{ kind: "READY"; snapshot: CacheScopeSnapshot }>
| Readonly<{ kind: "DISPOSED" }>;
export interface ServerStateScopeRuntime {
getSnapshot(): CacheScopeSnapshot;
subscribe(listener: () => void): () => void;
subscribeLifecycle(
listener: (event: ClientScopeLifecycleEvent) => void,
): () => void;
dispose(): void;
}
```
### 10.6 scope transition exact order
identity-affecting session change:
```text
1. generation increment
2. old snapshot isCurrent=false
3. FENCED synchronously publish
4. new HTTP/realtime/offline admission close
5. active Query cancel
6. QueryClient clear
7. conditional validator clear
8. mutation/invalidation pending lease clear
9. realtime stop/checkpoint detach
10. offline drain lease release and old partition hide
11. old identity registry close
12. new fingerprint/identity registry create
13. READY publish
14. React subscriber notify
15. new scope admission open
```
`FENCED``READY` 사이 old value를 render하지 않는다. UI는 `scope-transition` state를 사용한다.
### 10.7 query key
```text
[
"query",
1,
scopeFingerprint,
namespace,
namespaceVersion,
definitionVersion,
runtimeIdentityToken
]
```
raw account/resource ID, URL, filter object, document, cursor, message를 key에 넣지 않는다.
runtime identity registry bounds:
```text
max depth 12
max nodes 512
max string bytes 2,048
max encoded bytes 16,384
entries 4,096
canonical bytes 4 MiB
```
active reference가 있는 identity를 evict하지 않는다. capacity 부족은 network 전 local failure다.
### 10.8 execution commit fence
```text
capture scope
-> acquire identity lease
-> execute application input with TanStack signal
-> scope current check
-> Result success check
-> measureResult
-> profile budget check
-> immutable value return
-> Query cache commit
-> finally identity release
```
scope가 stale면 successful response도 `SCOPE_FENCED`로 폐기한다.
### 10.9 UI states
```text
initial-loading
success-fresh
success-refreshing
success-stale-degraded
empty
known-problem
transport-error
contract-error
scope-transition
```
background refresh에서 complete data를 full-page skeleton으로 숨기지 않는다.
### 10.10 persistence
baseline:
```text
memory QueryClient = COMPOSED
query persistence = disabled
SSR hydration = NOT_SELECTED
```
Web Storage나 existing generic IndexedDB runtime에 Query payload를 자동 저장하지 않는다. persistence 요구는 VD-13 별도 selection을 통과해야 한다.
### 10.11 conditional validator
conditional validator store는 Query value와 분리한다.
- exact operation/representation/scope/release binding;
- max entries 1,024;
- max token bytes 1,024;
- LRU zero-ref eviction;
- session transition clear;
- 304는 current complete cached value가 있을 때만 success;
- cache miss + 304는 contract/recovery failure;
- raw ETag를 Query key/UI/log에 넣지 않음.
---
## 11. Mutation lifecycle
### 11.1 mutation definition
```ts
export interface MutationDefinition<Input, Value> {
readonly definitionId: string;
readonly operationId: string;
readonly duplicatePolicy:
| "JOIN_IDENTICAL"
| "REJECT_WHILE_ACTIVE"
| "ALLOW_PARALLEL";
readonly optimisticPolicyId: "NONE" | string;
readonly invalidationTopics: readonly string[];
canonicalIdentity(input: Input): string;
execute(
input: Input,
context: Readonly<{
signal: AbortSignal;
intent: MutationIntent;
}>,
): Promise<MutationResult<Value>>;
}
export type MutationResult<Value> =
| Readonly<{
ok: true;
value: Value;
effect: "APPLIED_CONFIRMED";
}>
| Readonly<{
ok: false;
failure: AppFailure;
effect: "NOT_STARTED" | "NOT_APPLIED" | "MAYBE_APPLIED";
}>;
```
### 11.2 duplicate policy defaults
```text
command default = REJECT_WHILE_ACTIVE
query-like idempotent action = JOIN_IDENTICAL only when exact semantic input identity is stable
ALLOW_PARALLEL = explicit feature decision only
```
`JOIN_IDENTICAL`은 같은 scope, definition, canonical input과 same user intent에서만 가능하다. 서로 다른 input을 logical key 하나로 합치지 않는다.
### 11.3 duplicate coordinator bounds
```text
active definitions per runtime = 256
active intents total = 1,024
canonical identity bytes = 16,384
waiters per joined intent = 32
settled retention = 0 ms; immediate removal
```
coordinator map을 result cache로 사용하지 않는다.
### 11.4 optimistic default
`optimisticPolicyId=NONE`이 기본이다.
optimistic update 허용 조건:
- deterministic local projection;
- rollback snapshot bounded;
- server revision/canonical response가 최종 authority;
- list membership/sort/filter effect가 deterministic;
- effect unknown recovery가 정의됨;
- same query key layer order가 관리됨.
### 11.5 ordered optimistic layer
```text
layers per query key <= 8
single layer estimate <= 262,144 bytes
all layers per query key <= 2,097,152 bytes
layer age <= mutation total deadline + 60s
```
overflow면 pessimistic execution으로 전환한다. 기존 layer를 silent evict하지 않는다.
### 11.6 result ordering
success:
```text
1. external wire validation
2. feature mapper
3. scope current check
4. mutation effect confirmed
5. exact detail seed if registered
6. optimistic layer commit/remove
7. registered invalidation topics emit
8. local active Query refetch schedule
9. success UI/navigation
```
list/aggregate는 default invalidate다. deterministic membership policy가 없으면 direct patch하지 않는다.
### 11.7 failure ordering
`NOT_STARTED/NOT_APPLIED`:
```text
rollback optimistic layer
-> expose retryable failure
```
`MAYBE_APPLIED`:
```text
optimistic layer를 committed/rolled-back으로 단정하지 않음
-> effect-unknown overlay
-> inspect/reconcile
-> confirmed outcome 후 canonical cache update
```
### 11.8 local invalidation failure
server command가 committed된 뒤 local invalidation이 실패해도 mutation을 failure로 바꾸지 않는다.
```text
mutation result = success
cache health = degraded
bounded authoritative refetch schedule
```
command를 다시 보내지 않는다.
### 11.9 form submit
- double-submit coordinator 사용;
- dirty state는 `APPLIED_CONFIRMED` 뒤 clear;
- effect unknown이면 dirty state를 clear하지 않음;
- successful navigation은 invalidation scheduling 뒤;
- route blocker는 active effect-unknown recovery를 고려;
- browser abort가 server rollback 증거가 아님.
---
## 12. Cross-context invalidation
### 12.1 wire 의미
cross-tab wire는 invalidate-only hint다.
전송 금지:
```text
query key
query data
input/filter
cursor
validator
optimistic layer
mutation body
account ID
resource ID
```
전송:
```text
protocol version
release epoch
scope epoch
sequence
opaque topic ID
source instance ID digest
```
### 12.2 registry
```ts
export interface InvalidationRegistry {
readonly topics: readonly string[];
readonly namespaces: readonly string[];
readonly edges: readonly Readonly<{
topicId: string;
namespace: string;
}>[];
}
```
bounds:
```text
topics 256
namespaces 256
edges 1,024
topic fan-out 64
namespace fan-in 64
ID bytes 80
```
duplicate edge와 orphan topic/namespace를 startup에서 거절한다.
### 12.3 primary/fallback transport
```text
primary = BroadcastChannel
fallback = localStorage pulse + storage event
```
channel/key는 registry-issued opaque token을 사용한다.
fallback pulse:
```text
maximum 1,024 bytes
TTL 60 seconds
write then remove not used; latest bounded envelope overwrite
storageArea must equal localStorage
same-tab local dispatch is explicit, storage event를 기대하지 않음
```
### 12.4 ordering
source instance별 monotonic sequence를 사용한다.
- duplicate sequence: ignore;
- lower sequence: stale duplicate ignore;
- exact next: topic apply;
- gap: 모든 registered namespace stale mark;
- release/scope epoch mismatch: event ignore + local full stale transition;
- event는 authority가 아니라 refetch hint.
### 12.5 mutation coalescing
current local mutation lease 동안 같은 topic remote hint를 coalesce한다.
mutation settlement 후:
- one invalidation apply;
- active query bounded refetch;
- inactive query는 next mount/focus revalidate;
- remote hint 때문에 optimistic layer를 tab 간 복제하지 않는다.
### 12.6 lifecycle
- application runtime start에서 one host;
- session transition 시 scope epoch rotate;
- logout에서 old event ignore;
- pagehide bfcache는 host suspend, restore에서 duplicate listener 없이 resume;
- dispose는 listener/channel/timer 제거;
- hidden page도 invalidation hint 수신은 유지하되 refetch는 visibility policy를 따른다.
---
## 13. Realtime product composition
### 13.1 existing runtime reuse
다음 existing source를 common runtime으로 유지한다.
```text
src/contracts/realtime-events.ts
src/contracts/realtime-streams.ts
src/application/ports/realtime/*
src/adapters/realtime/event-codec.ts
src/adapters/realtime/event-consumer.ts
src/adapters/realtime/reconnect-coordinator.ts
src/adapters/realtime/live-poll-handoff-coordinator.ts
src/adapters/realtime/stream-coordinator.ts
src/adapters/realtime/sse/*
src/adapters/realtime/websocket/*
src/adapters/realtime/polling/*
src/adapters/web-push/*
```
RT-01~RT-04를 새 protocol로 다시 구현하지 않는다.
### 13.2 product contribution source
```text
src/features/<feature>/contracts/<feature>-realtime-contribution.ts
```
```ts
export interface InstalledRealtimeContribution {
readonly contributionId: string;
readonly featureId: string;
readonly contractSourcePackageId: string;
readonly stream: RealtimeStreamRegistration;
readonly eventEffects: readonly InstalledRealtimeEventEffect[];
readonly transport: InstalledRealtimeTransport;
}
export interface InstalledRealtimeEventEffect {
readonly eventType: string;
readonly mapperId: string;
readonly effect:
| "INVALIDATE_TOPICS"
| "APPLY_AUTHORITATIVE_DELTA"
| "EPHEMERAL_NOTIFICATION";
readonly invalidationTopics: readonly string[];
}
export type InstalledRealtimeTransport =
| Readonly<{ kind: "SSE"; endpointId: string }>
| Readonly<{ kind: "WEBSOCKET"; endpointId: string }>
| Readonly<{
kind: "POLLING";
operationId: string;
intervalMs: number;
}>;
```
endpoint/event/recovery meaning is imported from the external package/approved frontend contribution. Frontend does not invent a server path or event payload.
### 13.3 contribution validation
```text
contributions max 64
streams max 128
event types max 512
effects per event max 8
invalidation topics per event 32
```
validation:
- contribution ID unique;
- stream/event registration existing registry rules pass;
- external event type exists in installed contract contribution;
- event validator package belongs to `contractSourcePackageId`;
- mapper exists and input schema matches payload validator;
- invalidation topic exists;
- transport endpoint/operation exists in installed package contribution;
- polling interval 5,000..300,000ms;
- transport exactly one;
- fallback is explicit, not inferred from runtime support.
### 13.4 no mega port
SSE, WebSocket, Polling, Web Push는 다음 의미로 분리한다.
```text
SSE active page one-way stream
WebSocket active page duplex protocol
Polling visible finite HTTP scheduling
Web Push inactive/background user notification hint
```
하나의 `RealtimePort.connect(any)`로 합치지 않는다.
### 13.5 transport selection
기본 선택 규칙:
- one-way active stream: SSE;
- duplex application protocol: WebSocket;
- stream provider가 없고 bounded freshness로 충분: Polling;
- inactive user notification: Web Push;
- Web Push를 foreground state synchronization으로 사용 금지;
- Polling을 connection failure 자동 downgrade로 사용 금지;
- primary/fallback pair는 contribution의 `stream.fallback`이 명시한다.
### 13.6 product composition stop condition
다음 중 하나라도 없으면 product realtime을 composition하지 않는다.
```text
exact external package event descriptor
exact endpoint/operation descriptor
feature mapper
feature effect mapping
scope mode
recovery mode
selected transport
kill switch contribution
browser lifecycle owner
```
common source가 존재한다는 이유로 production bootstrap에 연결하지 않는다.
### 13.7 host topology
application runtime당 `RealtimeRuntimeHost` 하나다.
```ts
export interface RealtimeRuntimeHost {
acquire(subscription: RealtimeSubscriptionRequest): RealtimeSubscriptionLease;
start(): Promise<void>;
stop(reason: RuntimeStopReason): Promise<void>;
dispose(): Promise<void>;
}
```
transport connection은 stream registration 단위로 공유한다. component/route마다 socket을 만들지 않는다.
### 13.8 single-writer rule
동일 stream/scope에는 active delivery writer가 하나다.
```text
SSE or WebSocket writer
XOR
bounded Polling writer
```
handoff:
```text
old writer admission close
-> old in-flight settle/cancel
-> checkpoint snapshot
-> new writer recovery barrier
-> new writer OPEN
```
live와 polling이 동시에 same effect를 적용하지 않는다.
---
## 14. Connection·Reconnect·Resume
### 14.1 state machine
```ts
export type RealtimeConnectionState =
| "IDLE"
| "CONNECTING"
| "AUTHENTICATING"
| "OPEN_UNRESUMED"
| "RECOVERING"
| "OPEN"
| "BACKING_OFF"
| "PAUSED_OFFLINE"
| "PAUSED_HIDDEN"
| "DEGRADED"
| "FATAL"
| "STOPPED";
```
`OPEN_UNRESUMED`에서 application event effect를 실행하지 않는다.
### 14.2 connection success
connection success는 다음만 뜻한다.
```text
transport handshake completed
selected protocol accepted
connection generation current
```
다음을 뜻하지 않는다.
```text
authenticated subscription active
event gap recovered
server state latest
checkpoint durable
application effect applied
```
### 14.3 reconnect policy
exact default:
```ts
export const DEFAULT_REALTIME_RECONNECT_POLICY = Object.freeze({
baseDelayMs: 500,
maxDelayMs: 30_000,
maxAttempts: 10,
maxElapsedMs: 300_000,
stableOpenMs: 60_000,
});
```
- algorithm: full jitter;
- server not-before hint: local delay lower bound;
- hint valid range: 0..60,000ms;
- remaining episode budget 초과 hint: reconnect 종료;
- attempts/exhausted: `DEGRADED`, explicit recovery action;
- automatic new episode cooldown: 300,000ms;
- valid event/heartbeat 또는 60s stable open 후 attempt counter reset;
- offline state에서는 timer 대신 `online` signal을 기다림.
### 14.4 error classification
reconnect candidate:
```text
network close
provider unavailable
retryable server close
idle timeout
validated auth refresh-ready transition
```
fatal:
```text
protocol mismatch
unknown required event version
schema/mapper runtime failure
forbidden
invalid endpoint
repeated recovery invariant failure
```
rate limited:
- validated delay 사용;
- maximum delay/episode budget 안에서만 retry;
- no immediate retry storm.
### 14.5 visibility policy
```text
VISIBLE -> normal connection/recovery
HIDDEN -> 30,000ms grace
HIDDEN after grace -> close transport, PAUSED_HIDDEN
VISIBLE restore -> authoritative recovery before OPEN
```
hidden 상태에서 Polling fallback을 시작하지 않는다. Web Push는 별도 capability다.
### 14.6 network hint
`navigator.onLine`은 hint다.
- `offline` event: new connection/retry 중지, active transport close;
- `online` event: immediate recovery admission one time;
- online인데 connect failure면 ordinary backoff;
- offline인데 existing request가 성공해도 network authority를 재정의하지 않음.
### 14.7 resume authority
resume mode는 existing `RealtimeRecoveryProfile` literal을 유지한다.
```text
CURSOR
SNAPSHOT_ONLY
SESSION_REBUILD
```
frontend owns:
- checkpoint storage lifecycle;
- connection generation;
- duplicate/gap detection;
- barrier ordering;
- snapshot application ordering.
external package/provider owns server cursor/replay/snapshot semantics. Frontend는 cursor를 decode/modify하지 않는다.
### 14.8 recovery barrier
`CURSOR`:
```text
open transport
-> send/attach opaque checkpoint through selected adapter
-> replay begins
-> validate/apply events sequentially
-> replay terminal barrier
-> OPEN
```
`SNAPSHOT_ONLY`:
```text
open transport or hold/buffer
-> authoritative snapshot query
-> scope current check
-> apply snapshot
-> release buffered event barrier
-> OPEN
```
`SESSION_REBUILD`:
```text
close old stream
-> rebuild feature input
-> reset feature state
-> fresh subscription
-> OPEN
```
barrier가 `NONE`인 snapshot profile도 connection open만으로 latest라고 표시하지 않는다. snapshot operation 성공이 필요하다.
### 14.9 checkpoint record
```ts
export interface RealtimeCheckpointRecord {
readonly recordVersion: 1;
readonly streamId: string;
readonly scopeBinding: string;
readonly connectionGeneration: number;
readonly opaqueCheckpoint: string;
readonly lastAppliedEventIdentity: string;
readonly writtenAt: string;
readonly expiresAt: string;
readonly contractPackageDigest: `sha256:${string}`;
}
```
bounds:
```text
checkpoint UTF-8 <= 4,096 bytes
record encoded <= 16,384 bytes
TTL <= 7 days
records per stream/scope = 1
```
credential, endpoint, raw account ID를 넣지 않는다.
### 14.10 gap handling
다음은 gap이다.
- expected sequence보다 큼;
- same sequence different fingerprint;
- cursor expired;
- dedupe window overflow;
- queue overflow;
- checkpoint version/package mismatch;
- connection generation conflict.
처리:
```text
new event admission stop
-> queued unapplied delta discard
-> state = RECOVERING
-> authoritative snapshot/session rebuild
-> checkpoint replace after effect
-> OPEN
```
gap 이후 delta를 계속 best-effort 적용하지 않는다.
### 14.11 SSE specifics
- Fetch streaming adapter reuse;
- response status/media validate;
- maximum line/event bytes는 common event bound;
- UTF-8 streaming decode;
- incomplete terminal event discard;
- heartbeat/comment는 payload event가 아님;
- idle deadline 45s default;
- total connection deadline 없음, lifecycle controls close;
- server `retry` hint is lower bound, max 60s;
- `Last-Event-ID` usage is adapter/profile-owned, opaque only.
### 14.12 WebSocket specifics
- exact URL/profile from installed endpoint;
- binary/text mode fixed per registration;
- subprotocol allowlist exact;
- outbound command disabled unless separate registered protocol;
- max event bytes 64KiB baseline;
- bufferedAmount high watermark 256KiB for outbound control messages;
- heartbeat expected 30s, idle timeout 45s when protocol declares heartbeat;
- browser WebSocket API가 backpressure를 보장한다고 가정하지 않음;
- receive queue hard bounds are common runtime authority;
- close code/reason raw text를 UI/log에 노출하지 않음.
### 14.13 bounded polling specifics
- existing registered terminal query operation only;
- visible page only;
- default interval 30s;
- minimum 5s, maximum 5m;
- one in-flight request;
- next schedule after previous settlement;
- hidden/offline pause;
- full jitter ±10% schedule spread;
- Query retry disabled, HTTP transport owns attempt;
- polling result enters same mapper/scope/result budget;
- polling is freshness policy, not realtime delivery guarantee.
---
## 15. Event validation과 subscription cleanup
### 15.1 hard limits
existing constants are normative.
```text
max event bytes 64 KiB
max payload depth 16
max payload nodes 4,096
max queue events 256
max queue bytes 4 MiB
max dedupe entries 2,048
max dedupe bytes 4 MiB
dedupe TTL 10 min
```
feature contribution can reduce but not raise them.
### 15.2 validation pipeline
```text
raw frame
-> frame byte bound
-> protocol decode
-> common envelope validation
-> stream registration lookup
-> event type membership check
-> package payload validator
-> feature mapper
-> scope binding/generation check
-> duplicate/sequence/gap decision
-> queue admission
-> sequential effect
-> effect success
-> checkpoint transaction commit
-> subscriber notification
```
순서를 바꾸지 않는다.
### 15.3 envelope/payload failure
```text
MALFORMED_FRAME
EVENT_TOO_LARGE
UNKNOWN_EVENT_TYPE
EVENT_NOT_ALLOWED_FOR_STREAM
ENVELOPE_SCHEMA_INVALID
PAYLOAD_SCHEMA_INVALID
VALIDATOR_RUNTIME_FAILURE
MAPPING_CONTRACT_VIOLATION
SCOPE_PROTOCOL_VIOLATION
```
unknown event를 generic object로 application에 전달하지 않는다.
state-bearing event failure:
```text
admission stop -> recovery
```
ephemeral notification failure:
```text
drop + bounded observation
```
### 15.4 duplicate/conflict
same event identity + same semantic fingerprint:
```text
duplicate -> no effect, no checkpoint regression
```
same identity + different fingerprint:
```text
EVENT_CONFLICT -> recovery
```
lower sequence but unknown dedupe entry:
```text
STALE_EVENT -> drop only when registration semantics allow
otherwise recovery
```
### 15.5 queue
- admission count/bytes atomic in host;
- event processing sequential per stream;
- different streams can progress independently;
- queue overflow triggers recovery, not oldest-drop;
- no unbounded `Promise.then` chain retained after settlement;
- queued item owns immutable mapped event and byte estimate;
- shutdown aborts admission and drains up to 2s, then drops unapplied items without checkpoint.
### 15.6 effect types
`INVALIDATE_TOPICS`:
```text
registered opaque topics only
-> local invalidation coordinator
-> active Query bounded refetch
```
`APPLY_AUTHORITATIVE_DELTA`:
- feature-owned reducer;
- current immutable snapshot and revision required;
- deterministic/no throw;
- result size budget;
- failure triggers recovery;
- partial delta not stored as event history.
`EPHEMERAL_NOTIFICATION`:
- no server-state authority;
- bounded UI notification;
- missed notification does not require state recovery unless separately marked state-bearing.
### 15.7 checkpoint commit ordering
금지:
```text
checkpoint write
-> application effect
```
필수:
```text
validate/map
-> effect commit
-> scope current recheck
-> checkpoint transaction commit
-> notify
```
checkpoint write failure after effect:
- event effect success를 rollback할 수 없음;
- state `RECOVERING`;
- next connection uses authoritative snapshot;
- same delta replay에 reducer idempotency를 가정하지 않음.
### 15.8 subscription lease
```ts
export interface RealtimeSubscriptionLease {
readonly leaseId: string;
readonly streamId: string;
readonly state: "ACTIVE" | "RELEASED";
release(): void;
}
```
key:
```text
streamId + scope generation + subscription projection identity
```
ref-count behavior:
```text
0 -> 1 : server/transport subscription admission
1 -> N : local subscriber only
N -> 1 : local release
1 -> 0 : 5s grace
0 after grace: unsubscribe/connection policy evaluation
```
### 15.9 cleanup triggers
immediate release:
- explicit component cleanup;
- route removal;
- feature disable;
- scope fence/logout;
- application shutdown;
- fatal protocol failure.
grace release:
- ordinary React remount/navigation;
- StrictMode setup-cleanup-setup;
- short route transition.
scope fence는 grace를 사용하지 않는다.
### 15.10 React bridge
component uses feature hook only.
```text
Component
-> useFeatureRealtime
-> acquire lease in effect
-> cleanup release
-> read external store via useSyncExternalStore
```
component does not:
- open/close socket;
- store cursor;
- schedule reconnect;
- parse event;
- call QueryClient directly.
### 15.11 StrictMode
required behavior:
```text
setup A acquire
cleanup A release -> grace starts
setup B acquire within grace -> same runtime retained
after final cleanup -> 5s -> actual unsubscribe
```
listener/ref count cannot go negative. release is idempotent.
### 15.12 bfcache
`pagehide persisted=true`:
- new event effect admission close;
- transport close;
- checkpoint remains;
- lease registry retained in suspended state.
`pageshow persisted=true`:
- release manifest refresh;
- scope current check;
- authoritative resume;
- leases reactivate after OPEN;
- duplicate listeners/connections 없음.
---
## 16. Generic Web Worker
### 16.1 capability boundary
Generic CPU Worker is separate from:
```text
OPFS dedicated worker
Service Worker
Web Push handler
SharedWorker
worklet
```
existing OPFS worker source is not moved into generic runtime.
### 16.2 selection condition
Generic Worker remains `NOT_SELECTED` until:
- measured main-thread task exceeds named interaction budget;
- task is CPU-bound;
- input/output/cancellation owner exists;
- safe main-thread fallback is defined or unsupported state is accepted;
- static `InstalledWebWorkerContribution` exists.
network I/O task, ordinary JSON validation, small mapper work를 worker로 옮기지 않는다.
### 16.3 source layout
```text
src/application/ports/web-worker/
worker-task-port.ts
src/contracts/web-worker.ts
src/adapters/web-worker/
web-worker-runtime.ts
web-worker-client.ts
web-worker-protocol.ts
web-worker-entry.ts
web-worker-task-registry.ts
```
application port is semantic task-specific, not generic arbitrary function execution.
### 16.4 installed contribution
```ts
export interface InstalledWebWorkerContribution {
readonly taskGroupId: string;
readonly tasks: readonly InstalledWorkerTask[];
readonly fallback: "MAIN_THREAD_CHUNKED" | "UNSUPPORTED";
}
export interface InstalledWorkerTask {
readonly taskId: string;
readonly taskVersion: 1;
readonly maximumInputBytes: number;
readonly maximumOutputBytes: number;
readonly deadlineMs: number;
}
```
bounds:
```text
task groups max 16
tasks per group 32
input default 1 MiB
input hard 8 MiB
output default 1 MiB
output hard 8 MiB
deadline default 5 s
deadline hard 30 s
```
### 16.5 worker topology
- task group당 worker 1개;
- baseline pool size 1;
- same group FIFO;
- groups can run independently;
- max active groups 4;
- no shared global pool;
- first task에서 lazy create;
- idle 60s with no active/queue -> terminate;
- next task creates new generation.
### 16.6 protocol V1
```ts
export type WorkerRequestMessage = Readonly<{
protocol: "CA_WEB_WORKER_V1";
kind: "EXECUTE";
taskId: string;
taskVersion: 1;
requestId: string;
workerGeneration: number;
deadlineEpochMs: number;
payload: unknown;
}>;
export type WorkerCancelMessage = Readonly<{
protocol: "CA_WEB_WORKER_V1";
kind: "CANCEL";
requestId: string;
workerGeneration: number;
}>;
export type WorkerResponseMessage =
| Readonly<{
protocol: "CA_WEB_WORKER_V1";
kind: "SUCCESS";
requestId: string;
workerGeneration: number;
payload: unknown;
}>
| Readonly<{
protocol: "CA_WEB_WORKER_V1";
kind: "FAILURE";
requestId: string;
workerGeneration: number;
code: WorkerFailureCode;
}>;
```
### 16.7 failure code
```text
TASK_UNKNOWN
VERSION_UNSUPPORTED
INPUT_INVALID
INPUT_TOO_LARGE
OUTPUT_INVALID
OUTPUT_TOO_LARGE
QUEUE_FULL
DEADLINE_EXCEEDED
CANCELLED
CRASHED
TRANSFER_FAILED
STALE_RESULT
RUNTIME_PROTOCOL_FAILURE
```
```ts
export type WorkerFailureCode =
| "TASK_UNKNOWN"
| "VERSION_UNSUPPORTED"
| "INPUT_INVALID"
| "INPUT_TOO_LARGE"
| "OUTPUT_INVALID"
| "OUTPUT_TOO_LARGE"
| "QUEUE_FULL"
| "DEADLINE_EXCEEDED"
| "CANCELLED"
| "CRASHED"
| "TRANSFER_FAILED"
| "STALE_RESULT"
| "RUNTIME_PROTOCOL_FAILURE";
```
native error message/stack is not posted to page.
### 16.8 queue bounds
```text
queued tasks per group 32
queued encoded bytes 16 MiB
active tasks per group 1
waiters per request 1
```
queue full is a local typed failure. It does not create another worker.
### 16.9 cancellation
queued:
- remove immediately;
- settle `CANCELLED`;
- no worker message.
active:
```text
send CANCEL
-> cooperative task checks signal
-> 250ms grace
-> not settled: terminate worker
-> all active/queued in same generation settle CRASHED/CANCELLED
-> next task creates new generation subject to restart budget
```
### 16.10 crash/restart budget
```text
maximum restarts = 3 per 5 minutes per task group
fourth crash -> group FATAL
manual retry -> new 5-minute window only after user action/page reload
```
crash does not retry completed/unknown task automatically.
### 16.11 structured clone/transfer
- message schema validates cloned shape;
- credentials, AbortSignal, class instance, function, DOM node prohibited;
- ArrayBuffer transfer allowed only when task declares ownership transfer;
- sender must treat transferred buffer as detached immediately;
- output transfer is validated after receive;
- SharedArrayBuffer baseline prohibited;
- transferable list count max 16.
### 16.12 stale result fence
result is accepted only when all match.
```text
requestId
workerGeneration
current task lease
caller scope generation
not cancelled
before deadline
```
stale result is dropped and observed. Query/UI is not updated.
### 16.13 worker network prohibition
worker entry cannot import HTTP adapter or use `fetch`, WebSocket, EventSource, credential/session adapter.
CPU task requiring remote input:
```text
page/application HTTP
-> validate/map/bound payload
-> Worker task
-> validate result
-> application/UI
```
### 16.14 main-thread fallback
`MAIN_THREAD_CHUNKED` only:
- same semantic task interface;
- chunk budget max 8ms;
- yield via scheduler/MessageChannel adapter;
- same total deadline;
- same cancellation/result validator;
- input hard max reduced to 1MiB;
- not presented as equivalent performance guarantee.
`UNSUPPORTED` returns typed unavailable; it does not execute synchronously.
---
## 17. Service Worker lifecycle
### 17.1 one physical registration
same scope has one worker entry.
```text
PWA lifecycle
verified static asset fetch
Web Push event/click
optional sync wake-up
```
separate registration prohibited.
```text
src/adapters/service-worker/service-worker-entry.ts
```
existing Web Push service-worker runtime becomes a handler factory inside this entry.
### 17.2 source layout
```text
src/contracts/service-worker.ts
src/adapters/service-worker/
service-worker-entry.ts
service-worker-protocol.ts
service-worker-page-controller.ts
service-worker-lifecycle.ts
service-worker-static-assets.ts
service-worker-sync-handler.ts
service-worker-removal.ts
src/bootstrap/register-service-worker.ts
```
### 17.2.1 TypeScript execution realms
main DOM, generic Web Worker와 Service Worker는 같은 TypeScript global lib를 공유하지 않는다.
```text
tsconfig.app.json lib = DOM + ES
tsconfig.web-worker.json lib = WebWorker + ES
include generic worker entry/runtime only
tsconfig.service-worker.json lib = WebWorker + ES
include service-worker entry/handlers only
```
- worker tsconfig에서 DOM global type을 사용하지 않는다;
- service-worker tsconfig에서 React/presentation/bootstrap page code를 include하지 않는다;
- shared protocol file은 DOM/WebWorker global type 대신 structural types만 사용한다;
- root `tsconfig.json` project references에 두 config를 추가한다;
- `skipLibCheck`로 realm collision을 숨기지 않는다.
### 17.2.2 deterministic two-pass build
Service Worker가 exact hashed app asset 목록을 compile-time으로 가져야 하므로 build 순서를 다음으로 고정한다.
```text
1. clean dist and .generated/frontend-runtime
2. generate contractSet + build-info source
3. Vite app build (emptyOutDir=true)
4. scan app dist and generate static asset source
5. ACTIVE mode일 때 Vite Service Worker build (emptyOutDir=false)
6. generate Release Manifest V2/build manifest
```
source:
```text
scripts/build-frontend.ts
scripts/generate-contract-set.ts
scripts/generate-service-worker-assets.ts
vite.service-worker.config.ts
.generated/frontend-runtime/contract-set.ts
.generated/frontend-runtime/service-worker-build-info.ts
.generated/frontend-runtime/service-worker-assets.ts
```
Service Worker build rules:
- input exactly `src/adapters/service-worker/service-worker-entry.ts`;
- output exactly `dist/service-worker.js` relative to deployed app root;
- ES module format;
- `inlineDynamicImports=true`;
- no additional worker chunk;
- sourcemap off by default;
- minification uses same pinned Vite toolchain;
- `REMOVE_REGISTRATION`, `PURGE_OWNED_RESOURCES`, `null` mode does not run active worker build;
- generated asset source excludes `service-worker.js` itself and `index.html`.
static delivery requirement:
```text
service-worker.js Cache-Control = no-cache
runtime config/release manifest = no-store
hashed assets = immutable
```
runtime does not fetch hosting policy to prove it. Static delivery configuration owns this requirement.
### 17.3 static selection modes
#### `ACTIVE`
handlers contain one or more unique values.
```text
WEB_PUSH
PWA_STATIC_ASSETS
OFFLINE_SYNC_WAKEUP
```
worker entry and register code are included.
#### `REMOVE_REGISTRATION`
- worker entry not included;
- cleanup controller included;
- exact registration scope/script ownership check;
- unregister only;
- caches retained.
#### `PURGE_OWNED_RESOURCES`
- repeat unregister check;
- exact owned caches/metadata purge;
- outbox/OPFS/file data untouched.
#### `null`
- entry and cleanup code absent;
- registration lookup 0;
- Cache Storage access 0.
transition:
```text
ACTIVE
-> REMOVE_REGISTRATION for at least one deployed release
-> PURGE_OWNED_RESOURCES after rollback retention
-> null
```
### 17.4 exact ownership check
expected:
```text
scope = new URL(routerBasePath, location.origin).href
script = new URL("service-worker.js", scope).href
```
`getRegistration(routerBasePath)` result is owned only if:
- registration.scope exact equals expected scope;
- every present installing/waiting/active worker scriptURL is same-origin;
- at least one present worker scriptURL exact equals expected script;
- no present worker has different script URL.
ownership mismatch means no unregister/purge.
### 17.5 registration timing
```text
Runtime Config valid
-> Release Manifest/contractSet valid
-> base composition
-> React first committed effect
-> PostMountRuntimeStarter.start
-> ACTIVE/cleanup mode action
```
`ACTIVE` registration call:
```ts
const expectedScope = new URL(routerBasePath, location.origin);
const scriptUrl = new URL("service-worker.js", expectedScope);
navigator.serviceWorker.register(scriptUrl, {
scope: expectedScope.pathname,
type: "module",
updateViaCache: "none",
});
```
StrictMode repeat returns same in-flight promise.
### 17.6 runtime override `DISABLED`
static `ACTIVE` + config disabled:
```text
new register 0
worker message/update timer 0
sync register 0
push handler activation 0
exact-owned registration lookup 1
owned registration unregister <= 1
owned cache delete 0
```
failure code `DISABLE_CLEANUP_FAILED`; capability remains inactive. next clean navigation may retry once, no loop.
### 17.7 protocol identity
```text
serviceWorkerProtocolVersion = 1
cacheSchemaVersion = 1
buildId
releaseId
contractSetDigest
staticAssetSetDigest
```
page and worker read the same compile-time virtual build-info module.
### 17.8 message protocol
```text
PAGE_HELLO
WORKER_HELLO_ACK
UPDATE_READY
ACTIVATE_REQUEST
ACTIVATE_ACCEPTED
ACTIVATE_REJECTED
CLIENT_DRAIN_REQUEST
CLIENT_DRAINED
ACTIVATED_RELOAD_REQUIRED
CACHE_RESET_REQUEST
CACHE_RESET_RESULT
SYNC_WAKE_OBSERVED
```
common fields:
```text
protocolVersion = 1
messageId = UUID
sourceBuildId
targetBuildId optional
nonce optional
```
origin/source client ID, exact message keys, runtime schema and one-time nonce validation required.
### 17.9 install
without static asset handler:
- Cache Storage open 0;
- lifecycle/push/sync handlers register only.
with static assets:
```text
1. embedded manifest validate
2. candidate cache open
3. fetch concurrency 4
4. status/content-type/bytes/integrity verify
5. candidate marker write
6. all success -> install resolve
7. any failure -> candidate cache delete and install reject
```
bounds:
```text
assets count 256
single asset 2 MiB
asset set 5 MiB
concurrency 4
install deadline 60 s
```
partial candidate is never used.
### 17.10 waiting update
waiting worker sends `UPDATE_READY` to controlled clients.
it does not call `skipWaiting()` itself.
page blocks activation when:
- dirty form;
- active mutation;
- effect unknown recovery;
- offline command `SENDING`;
- unresolved local file write;
- incompatible protocol tuple.
### 17.11 client drain activation
```text
page creates one-time nonce
-> ACTIVATE_REQUEST(expected tuple)
-> waiting worker validates
-> CLIENT_DRAIN_REQUEST to all controlled window clients
-> each client closes new mutation/realtime/offline admission
-> active effects settle
-> CLIENT_DRAINED(nonce)
-> all clients within 30s
-> ACTIVATE_ACCEPTED
-> waiting worker skipWaiting
-> activate event
-> no clients.claim
-> ACTIVATED_RELOAD_REQUIRED
-> clean page user/guarded reload once
```
one missing client rejects automatic activation.
### 17.12 no clients.claim
baseline does not call `clients.claim()`.
- first install page is not silently controlled;
- update does not mix new worker with old page without reload;
- current page becomes controlled after explicit clean reload;
- future claim adoption requires separate compatibility decision.
### 17.13 first install
Service Worker reload guard is distinct from chunk recovery.
```text
storage key = SERVICE_WORKER_RELOAD_GUARD
value schema = { version: 1, oldBuildId, newBuildId, writtenAt }
maximum bytes = 512
TTL = 10 minutes
```
`CHUNK_RELOAD_GUARD`를 재사용하지 않는다. Storage registry/codec이 exact key와 envelope를 소유한다.
if active tuple matches current build but `navigator.serviceWorker.controller` is null:
```text
state = RELOAD_TO_ENABLE
```
- no automatic reload;
- user action only when page clean;
- reload guard once per build;
- after reload `PAGE_HELLO` handshake required;
- handshake failure -> `INCOMPATIBLE`, capability inactive.
### 17.14 update checking
- no immediate repeated `registration.update()`;
- visible page at most once per 6 hours;
- manual action once independently rate-limited;
- hidden page no scheduled update;
- network failure does not fail product flow;
- controllerchange reload guard `(oldBuild,newBuild)` once.
### 17.15 activate cleanup
- candidate marker and tuple validate;
- current revision active;
- previous verified revision retain one;
- older owned cache delete;
- exact prefix + parser only;
- config/release/API cache found -> security violation and delete;
- unregister != cache cleanup.
### 17.16 page controller interface
```ts
export type ServiceWorkerStartOutcome =
| Readonly<{ kind: "ACTIVE"; buildId: string }>
| Readonly<{ kind: "RELOAD_TO_ENABLE" }>
| Readonly<{ kind: "UPDATE_WAITING" }>
| Readonly<{ kind: "DISABLED" }>
| Readonly<{ kind: "INCOMPATIBLE" }>
| Readonly<{ kind: "FAILED"; code: string }>;
export type ServiceWorkerActivationOutcome =
| Readonly<{ kind: "ACTIVATED_RELOAD_REQUIRED" }>
| Readonly<{ kind: "BLOCKED_DIRTY_CLIENT" }>
| Readonly<{ kind: "CLIENT_DRAIN_TIMEOUT" }>
| Readonly<{ kind: "NO_WAITING_WORKER" }>
| Readonly<{ kind: "PROTOCOL_MISMATCH" }>
| Readonly<{ kind: "FAILED"; code: string }>;
export type ServiceWorkerResetOutcome =
| Readonly<{ kind: "RESET"; cachesDeleted: number }>
| Readonly<{ kind: "NOT_CONTROLLED" }>
| Readonly<{ kind: "PROTOCOL_MISMATCH" }>
| Readonly<{ kind: "FAILED"; code: string }>;
export interface ServiceWorkerRuntimeHost {
start(): Promise<ServiceWorkerStartOutcome>;
requestActivation(): Promise<ServiceWorkerActivationOutcome>;
resetOwnedCaches(): Promise<ServiceWorkerResetOutcome>;
stop(): Promise<void>;
}
```
`stop()` removes page listener/update timer. ordinary shutdown does not unregister.
### 17.17 cleanup mode outcomes
```ts
export type ServiceWorkerRemovalOutcome =
| Readonly<{ kind: "ABSENT" }>
| Readonly<{ kind: "UNREGISTERED" }>
| Readonly<{ kind: "PURGED"; cachesDeleted: number; metadataDeleted: number }>
| Readonly<{ kind: "OWNERSHIP_MISMATCH" }>
| Readonly<{ kind: "FAILED"; operation: "LOOKUP" | "UNREGISTER" | "PURGE" }>;
```
foreign resource count can be observed only as bounded count, no names.
---
## 18. Cache Storage와 fetch policy
### 18.1 existing runtime reuse
```text
src/adapters/cache-storage/public-cache-policy.ts
src/adapters/cache-storage/public-response-cache-adapter.ts
```
no generic `CacheService`.
### 18.2 cache name
```text
ca-static-v1-<first-16-lower-hex-of-staticAssetSetDigest>
```
parser requires exact prefix, schema literal and 16 hex chars.
### 18.3 embedded static asset manifest
Vite virtual modules:
```text
virtual:ca-service-worker-assets
virtual:ca-service-worker-build-info
```
```ts
export interface StaticAssetManifestV1 {
readonly schemaVersion: 1;
readonly buildId: string;
readonly releaseId: string;
readonly setDigest: `sha256:${string}`;
readonly assets: readonly Readonly<{
url: string;
sha256: `sha256:${string}`;
bytes: number;
contentType: string;
}>[];
}
```
worker does not fetch a manifest endpoint.
### 18.4 manifest inclusion
include only:
- Vite immutable hashed JS/CSS assets;
- approved font/image assets with immutable hashed URL;
- worker-owned static metadata.
exclude:
```text
index.html
runtime config
release manifest
API response
user/account data
telemetry/auth endpoint
source map unless explicitly public
unhashed mutable asset
cross-origin opaque response
```
### 18.5 fetch classification order
```text
non-GET -> network passthrough
navigation request -> network-only
runtime config URL -> network-only
release manifest URL -> network-only
exact static asset manifest hit -> verified cache-first
all other requests -> network passthrough
```
API base can be `/`; exact static hit is evaluated before generic network passthrough.
### 18.6 cache hit validation
- exact URL;
- method GET;
- only build-generated query if manifest contains it;
- cached marker matches set digest;
- status 200;
- content type exact normalized match;
- no opaque response;
- expected byte count;
- install-time integrity verified.
invalid hit is deleted and treated as release mismatch/network fetch. It is not returned stale.
### 18.7 cache miss
runtime fetch success is returned but not written into active cache. Static cache population is install-time only.
missing immutable asset:
```text
network failure/404
-> release mismatch classification
-> page chunk recovery path
```
### 18.8 navigation
baseline has no offline shell.
- navigation network-only;
- cached `index.html` fallback prohibited;
- network error uses browser/app boot failure;
- stale HTML and new assets/contractSet never intentionally mix;
- installability/offline document is a separate product capability.
### 18.9 quota
- asset set hard max 5MiB;
- `StorageManager.estimate()` is advisory only;
- actual `QuotaExceededError` is authoritative;
- install quota failure deletes candidate;
- existing current/previous cache retained;
- no browser file/OPFS/outbox cleanup to make room;
- user data has higher priority than recomputable static cache.
### 18.10 reset
`CACHE_RESET_REQUEST` requires:
- controlled same-origin client;
- current protocol;
- one-time nonce;
- user/incident action;
- owned prefix parser.
reset deletes static caches only. registration remains. reload after reset is user/guarded action.
---
## 19. Offline Command와 Background Sync
### 19.1 capability status
baseline product selection remains `NOT_SELECTED`.
reference runtime can be implemented without composing a product operation. It must not create DB/listener when unselected.
### 19.2 scope
Offline Command is for durable user command retry after foreground interruption. It is not:
- Query persistence;
- arbitrary request cache;
- background execution guarantee;
- exactly-once guarantee;
- event queue;
- file upload checkpoint replacement.
### 19.3 external prerequisite consumption
an operation is queueable only when external package contribution provides:
```text
retrySemantics = KEYED
commandRecovery non-null
stable input validator/projection
stable operation identity
runtime validator for terminal result/problem
```
frontend does not define server idempotency or inspect protocol.
### 19.4 source layout
```text
src/application/ports/offline-command/
offline-command-port.ts
src/contracts/offline-command.ts
src/adapters/offline-command/
offline-command-codec.ts
indexeddb-offline-command-repository.ts
offline-command-drain.ts
offline-command-lifecycle.ts
offline-command-summary.ts
```
existing generic IndexedDB runtime is reused as dataset backend.
### 19.5 installed contribution
```ts
export interface InstalledOfflineCommandContribution {
readonly datasetId: "OFFLINE_COMMANDS_V1";
readonly operations: readonly InstalledOfflineOperation[];
}
export interface InstalledOfflineOperation {
readonly operationId: string;
readonly contractPackageId: string;
readonly maximumRequestBytes: number;
readonly retentionClass: "STANDARD_7D";
}
```
bounds:
```text
operations 64
single request 256 KiB
hard single request 1 MiB
records 1,000
dataset bytes 50 MiB
```
### 19.6 record
```ts
export interface OfflineCommandRecordV1 {
readonly recordVersion: 1;
readonly commandId: string;
readonly operationId: string;
readonly contractPackageId: string;
readonly contractPackageVersion: string;
readonly contractPackageDigest: `sha256:${string}`;
readonly scopePartition: string;
readonly requestDigest: `sha256:${string}`;
readonly requestPayload: Uint8Array;
readonly idempotencyKey: string;
readonly state: OfflineCommandState;
readonly attempt: number;
readonly createdAt: string;
readonly updatedAt: string;
readonly nextAttemptAt?: string;
readonly leaseOwner?: string;
readonly leaseExpiresAt?: string;
readonly terminalCode?: string;
}
```
```ts
export type OfflineCommandState =
| "PENDING"
| "LEASED"
| "FOREGROUND_REQUIRED"
| "SENDING"
| "RETRY_WAIT"
| "ACKED"
| "CONFLICT"
| "EFFECT_UNKNOWN"
| "EXPIRED";
```
### 19.7 physical storage
- opaque DB/dataset name from existing governance;
- account/session scope partition binding;
- transaction-complete is write success;
- record codec V1 strict;
- future schema fail closed/read-only;
- blocked/versionchange typed;
- no credential/token except opaque idempotency key;
- request payload minimized to exact package request projection;
- no raw URL/header.
### 19.8 enqueue
```text
scope current
-> operation allowlist
-> package identity current
-> package input validator
-> deterministic request encode
-> request bytes/digest
-> create idempotency key
-> dataset count/byte admission
-> one IDB transaction insert + counters
-> PENDING
```
same user intent duplicate uses feature/mutation coordinator before enqueue. Repository does not silently merge by payload digest.
### 19.9 sender topology
origin/account partition has one sender lease.
```text
lease TTL 30s
renew every 10s
batch count 10
batch window 30s
parallel send 1
```
foreground tabs coordinate through existing cross-context hint and IndexedDB lease CAS. BroadcastChannel is hint, IDB lease is local authority.
### 19.10 state transitions
```text
PENDING -> LEASED -> SENDING
SENDING -> ACKED
SENDING -> RETRY_WAIT
SENDING -> CONFLICT
SENDING -> EFFECT_UNKNOWN
PENDING/RETRY_WAIT -> FOREGROUND_REQUIRED
PENDING/RETRY_WAIT/FOREGROUND_REQUIRED -> EXPIRED
```
invalid transitions are corruption.
### 19.11 transaction boundaries
before network:
```text
IDB transaction:
verify current state
verify lease owner/live
increment attempt
state=SENDING
write operation attempt identity
commit
```
after terminal response:
```text
validate/map/effect classify
-> IDB transaction terminal/retry state commit
-> lease release
```
### 19.12 crash recovery
expired `LEASED` before send:
```text
safe -> PENDING
```
expired `SENDING`:
```text
EFFECT_UNKNOWN
```
never reset SENDING to PENDING automatically.
### 19.13 foreground drain triggers
```text
startup
online
visible
focus
manual
```
triggers coalesce into one scheduled drain. hidden page pauses new sends. active send result still settles.
### 19.14 auth and package fence
before each send:
- scope current;
- credential READY;
- installed contract package identity exact matches record;
- operation still installed;
- request payload revalidated;
- idempotency key present;
- total deadline available.
failure:
```text
auth unavailable -> FOREGROUND_REQUIRED
package missing/version/digest mismatch -> FOREGROUND_REQUIRED(CONTRACT_PACKAGE_UNAVAILABLE)
scope stale -> old partition hidden, no send
schema invalid -> CONFLICT/terminal corruption review
```
### 19.15 retry
retryable response/failure is package/HTTP runtime decision.
```text
base delay 1s
max delay 5m
full jitter
attempt cap 10
```
Retry-After is lower bound up to 5m.
never retry:
- contract/schema/mapper failure;
- effect maybe applied;
- auth unavailable;
- conflict;
- expired retention.
attempt cap -> `FOREGROUND_REQUIRED(RETRY_EXHAUSTED)`, not silent drop.
### 19.16 effect unknown
```text
automatic send stop
-> package recovery descriptor
-> foreground inspect when auth ready
-> committed: ACKED + canonical refresh
-> no effect: PENDING only after explicit transition
-> still unknown: EFFECT_UNKNOWN
```
manual retry button cannot resend EFFECT_UNKNOWN directly.
### 19.17 retention
```text
PENDING/RETRY_WAIT/FOREGROUND_REQUIRED max age 7d
ACKED payload delete immediately; safe summary 24h
CONFLICT/EFFECT_UNKNOWN payload max 30d
EXPIRED payload delete; safe summary 7d
```
30d 이후 unresolved payload를 자동 send하지 않는다.
### 19.18 Background Sync baseline
Background Sync is optional wake-up.
```text
sync tag = ca-outbox-v1
```
register only when:
- service worker handler `OFFLINE_SYNC_WAKEUP` selected;
- `registration.sync` exists;
- pending/foreground-required record exists;
- no in-flight same tag;
- last registration >= 1 minute.
sync handler:
```text
1. inspect bounded outbox metadata
2. update wakeRequestedAt
3. post SYNC_WAKE_OBSERVED to controlled clients
4. send authenticated command = 0
5. resolve handler
```
### 19.19 foreground fallback
Background Sync unsupported/fails/never fires is normal.
foreground triggers remain required. Queue semantics are identical with or without native Sync.
### 19.20 future worker send explicitly excluded
Service Worker authenticated send requires separate future design covering:
- worker-safe credential owner;
- no credential persistence;
- refresh/revocation;
- package validators in worker bundle;
- effect reconciliation;
- logout/version fence;
- platform execution limitations;
- user recovery.
until then worker send code is prohibited.
### 19.21 safe summary
```ts
export interface OfflineCommandSummary {
readonly commandId: string;
readonly operationLabelKey: string;
readonly state: OfflineCommandState;
readonly createdAt: string;
readonly nextAction:
| "WAIT"
| "OPEN_APP"
| "CHECK_STATUS"
| "RESOLVE_CONFLICT"
| "CONTACT_SUPPORT"
| "DISMISS";
}
```
raw payload, idempotency key, package digest, account partition are not exposed to UI.
### 19.22 removal
```text
1. new enqueue close
2. drain stop
3. active send settle/lease expire
4. unresolved count inspect
5. product recovery/export decision
6. selected dataset only delete when safe
7. sync registration stop
8. SW sync handler remove
9. composition remove
```
unresolved user command is not silently purged with source deletion.
---
## 20. 공통 start·stop·dispose
### 20.1 lifecycle interface
```ts
export interface RuntimeLifecycle {
start(): void | Promise<void>;
stop(reason: RuntimeStopReason): void | Promise<void>;
dispose(): void | Promise<void>;
}
export type RuntimeStopReason =
| "APPLICATION_SHUTDOWN"
| "SCOPE_FENCED"
| "FEATURE_DISABLED"
| "HIDDEN_POLICY"
| "INCIDENT_CONTAINMENT";
```
invariants:
- `start()` idempotent;
- concurrent start returns same promise;
- start failure cleans partial resource and state `FAILED`;
- `stop()` is restartable quiesce unless capability says terminal;
- `dispose()` terminal;
- dispose after dispose no-op;
- start after dispose throws typed lifecycle error;
- concurrent stop/dispose has one settlement;
- cleanup error aggregation does not skip remaining cleanup.
### 20.2 runtime lifecycle states
```text
NEW
STARTING
RUNNING
STOPPING
STOPPED
FAILED
DISPOSING
DISPOSED
```
allowed transitions:
```text
NEW -> STARTING -> RUNNING
STARTING -> FAILED
RUNNING -> STOPPING -> STOPPED
STOPPED -> STARTING
NEW/STOPPED/FAILED/RUNNING -> DISPOSING -> DISPOSED
```
`FAILED -> STARTING` automatic transition is prohibited. Explicit owner recovery creates a new runtime instance or calls a named reset method.
### 20.3 application shutdown order
```text
1. close new navigation/mutation/optional admission
2. stop Service Worker page controller/cleanup action
3. close Web Worker task admission
4. cooperative cancel/terminate Web Workers
5. stop realtime subscriptions/reconnect and drain
6. stop offline foreground drain and release lease
7. dispose client scope runtime
8. clear conditional validator store
9. dispose invalidation coordinator
10. cancel and clear QueryClient
11. close cross-context transport
12. dispose session subscription
13. telemetry flush up to deadline
14. diagnostics dispose
```
QueryClient를 realtime보다 먼저 clear하지 않는다. late realtime effect가 cleared cache를 다시 채우는 race를 막는다.
### 20.4 browser lifecycle owner
window listener owner:
```text
src/adapters/platform/browser-lifecycle.ts
```
single listener set:
```text
visibilitychange
online
offline
focus
pagehide
pageshow
beforeunload only while dirty state exists
```
capability별 direct window listener를 추가하지 않는다.
```ts
export interface BrowserLifecycleSnapshot {
readonly visibility: "VISIBLE" | "HIDDEN";
readonly connectivityHint: "ONLINE" | "OFFLINE";
readonly pageState: "ACTIVE" | "PAGEHIDE" | "BFCACHE_RESTORED";
readonly generation: number;
}
```
### 20.5 pagehide and bfcache
`pagehide persisted=true`:
- terminal dispose하지 않음;
- lifecycle state `SUSPENDED` projection;
- realtime transport close;
- offline new send pause;
- active Worker follows task cancellation policy;
- Query memory remains;
- Service Worker registration remains;
- no blocking telemetry flush.
`pageshow persisted=true`:
```text
1. browser lifecycle generation increment
2. Release Manifest V2 no-store refresh once
3. scope current check
4. active Query refetch
5. realtime authoritative recovery
6. offline expired lease recovery
7. duplicate listener/runtime check
```
manifest incompatible면 product runtime을 resume하지 않고 safe reload/recovery surface로 이동한다.
### 20.6 unload non-guarantee
다음을 `beforeunload`, `unload`, `sendBeacon` correctness로 처리하지 않는다.
- command complete;
- checkpoint write;
- subscription unsubscribe guarantee;
- lease release guarantee;
- Service Worker activation acknowledgement;
- telemetry guaranteed delivery.
correctness is transaction/TTL/recovery based.
### 20.7 cleanup deadlines
```text
realtime drain 2,000ms
worker cooperative stop 250ms per active group
telemetry flush 1,000ms
offline local lease release 2,000ms
Service Worker page stop no network wait
overall application stop 5,000ms
```
overall deadline expires:
- remaining worker terminate;
- transport close;
- listener/timer remove;
- unresolved durable state left for recovery;
- no additional network retry.
### 20.8 scope fence vs shutdown
scope fence:
- application remains alive;
- global Router/QueryClient object remains;
- account-bound data/listener/connection fenced;
- new scope READY after reset.
shutdown:
- all admission terminal close;
- no new scope;
- every runtime disposed.
두 lifecycle을 같은 `clearEverything()`로 합치지 않는다.
---
## 21. Security와 privacy
### 21.1 trust boundary
trusted build inputs:
```text
exact installed contract package bytes
frontend source
embedded contractSet identity
embedded static asset manifest
```
untrusted runtime inputs:
```text
Runtime Config bytes until validated
Release Manifest bytes until validated
HTTP response/problem
realtime frame/event
postMessage
IndexedDB historical record
Cache Storage response
URL/path/search/form input
```
trusted package does not make server response trusted. Runtime validation remains mandatory.
### 21.2 secret prohibition
must not contain credential/secret:
```text
Runtime Config
Release Manifest
contractSet
Query key/value
Web Storage
IndexedDB query metadata
Cache Storage
Worker message
Service Worker message
realtime checkpoint diagnostics
URL
telemetry attributes
support reference
```
offline outbox may store opaque idempotency key but treats it as sensitive command identity:
- no log;
- no UI;
- no cross-context message;
- delete with payload retention.
### 21.3 generated package isolation
contract package must:
- have no required postinstall script;
- perform no network/DOM/listener side effect on import;
- expose descriptor/validator/type only;
- have no mutable singleton carrying user state;
- not reference `window`, `document`, `navigator`, `fetch`, WebSocket at runtime;
- not include credential helper;
- not provide user-facing error copy.
violation blocks contribution composition.
### 21.4 boot request security
- same-origin Runtime Config/release manifest;
- redirect error;
- no credentials beyond same-origin browser policy;
- no-store;
- strict content type;
- byte bound;
- secret-name scan;
- safe error only;
- no arbitrary manifest URL from query/local storage.
### 21.5 HTTP security
- HTTPS outside local;
- final URL allowlist;
- redirect reject;
- `referrerPolicy=no-referrer`;
- credential patch allowlist;
- raw URL/query/body/header no logs;
- response byte bound;
- duplicate JSON key semantics not relied upon;
- prototype pollution keys rejected/stripped by validator/mapper;
- problem detail rendered through safe text;
- `dangerouslySetInnerHTML` forbidden.
### 21.6 Router security
- open redirect forbidden;
- return URL created only by same-origin route builder;
- route param validation before domain ID;
- frontend access guard not authorization proof;
- chunk URL not user-generated;
- search data not interpolated into raw HTML/style/script;
- malformed route causes local failure before network.
### 21.7 Query/cache security
- cache hit not authorization;
- old scope value hidden immediately at FENCED;
- raw IDs/URL/document/message absent from key;
- credential/header/trace absent from value;
- query persistence disabled;
- production devtools governed by build profile;
- optimistic layer excludes full command body/token.
### 21.8 Cross-context security
- no data/query key;
- opaque topic/source;
- protocol/epoch validation;
- `storageArea` exact check;
- message bytes bound;
- foreign/malformed event ignored;
- channel name not based on account ID;
- event is hint, not authority.
### 21.9 Realtime security
- exact endpoint/profile allowlist;
- frame/event bounds;
- envelope/payload validation;
- scope binding;
- unknown event safe recovery;
- cursor/endpoint/push subscription absent from diagnostics;
- no arbitrary WebSocket outbound command;
- open connection not auth success;
- raw close reason not user/log.
### 21.10 Worker security
- module worker static URL;
- Blob/data URL worker forbidden;
- `eval`, `Function`, dynamic `importScripts` forbidden;
- no credential/network;
- cloned payload validator;
- input/output bounds;
- generation/request fence;
- SharedArrayBuffer disabled;
- WASM only as separate exact-digest/memory-bound task capability.
### 21.11 Service Worker security
- same-origin scope/script;
- source client ID/origin/nonce validation;
- no blind skipWaiting/clients.claim;
- API/private response cache forbidden;
- opaque cache forbidden;
- owned prefix/parser cleanup only;
- global variable not durable authority;
- push payload validator and notification registry;
- notification URL registered same-origin route only;
- foreign registration never unregistered.
### 21.12 IndexedDB/outbox privacy
- opaque physical DB name;
- scope binding;
- historical codec validation;
- package digest recheck;
- quota/corrupt/future schema separated;
- logout old partition hidden immediately;
- same-origin encryption not XSS authorization boundary;
- payload minimization;
- retention ceiling;
- unresolved command export/recovery product-owned.
### 21.13 diagnostics redaction
prohibited values:
```text
raw URL/query
request/response body
header/token/cookie
account/resource ID
event payload/cursor
idempotency key
contract package source path
cache key/value
worker payload
file name/path
```
allowed dimensions are closed enums and bounded IDs declared in §22.
---
## 22. Runtime observability
### 22.1 ownership
existing diagnostics/telemetry adapters are reused.
```text
src/adapters/diagnostics/bounded-diagnostics.ts
src/adapters/telemetry/best-effort-telemetry.ts
src/contracts/diagnostics.ts
src/contracts/telemetry.ts
```
raw SDK is not exposed to feature/application.
### 22.2 closed event families
```text
frontend_boot
frontend_http
frontend_query
frontend_mutation
frontend_scope
frontend_realtime
frontend_worker
frontend_service_worker
frontend_offline_command
frontend_drop
```
new arbitrary event name is prohibited. Add a typed registry entry first.
### 22.3 common attributes
allowlisted:
```text
operationCategory
outcome
failureKind
appEnv
buildId bucket/release reference
featureId
operationId declared stable ID
profileId
transportKind
browserEngine bucket
queueSize bucket
payloadSize bucket
attempt bucket
lifecycleState
```
not allowlisted:
```text
URL
user/account/resource ID
request ID from server
cursor/checkpoint
package digest full value
idempotency key
message/error string
```
contract package digest may be represented only as `MATCH | MISMATCH | MISSING`, not tag value.
### 22.4 HTTP observation
one logical operation terminal event plus bounded physical attempt counters.
```text
logical operation ID
attempt count bucket
terminal outcome
certainty
request/response size bucket
latency bucket
retry reason category
```
physical attempts do not each emit high-cardinality traces by default.
### 22.5 Query observation
```text
profileId
cache state: MISS | FRESH | STALE | REFRESH
result budget: ADMITTED | REJECTED
scope current: CURRENT | FENCED
```
query key/input/value absent.
### 22.6 Mutation observation
```text
duplicate policy
duplicate outcome
optimistic used boolean
effect certainty
invalidation result
recovery state
```
intent/idempotency identity absent.
### 22.7 Realtime observation
```text
streamId stable registry ID
eventType stable registry ID
transport
state transition
reconnect attempt bucket
queue bucket
recovery reason
validation outcome
effect outcome
```
cursor/payload/endpoint absent.
### 22.8 Worker observation
```text
taskGroupId
taskId
queue bucket
input/output size bucket
latency bucket
cancel/crash/restart outcome
fallback mode
```
payload and native stack absent.
### 22.9 Service Worker observation
```text
mode
handler set bitmask or closed enum
registration outcome
update state
client drain outcome
cache install/reset outcome
owned removal outcome
```
scope/script URL/cache name absent. ownership mismatch is a boolean/outcome only.
### 22.10 Offline observation
```text
state transition
attempt bucket
age bucket
queue count/byte bucket
lease outcome
terminal/recovery category
sync wake observed boolean
```
operation label uses stable operation ID; no payload/key/partition.
### 22.11 bounded queue and failure
telemetry is best effort.
```text
maximum queued events 500
maximum estimated bytes 1 MiB
flush batch 50
shutdown flush 1s
drop oldest non-critical observation when full
never block product flow
never recursively report sink failure
```
security/contract fatal events can be mirrored to bounded local diagnostics but do not gain infinite retention.
### 22.12 health semantics
frontend does not expose one global `healthy` boolean.
```ts
export type RuntimeHealth =
| "AVAILABLE"
| "DEGRADED"
| "UNAVAILABLE"
| "INCOMPATIBLE"
| "DISABLED";
```
capability health is independent:
- HTTP unavailable does not mark Router unavailable;
- realtime fatal can fall back to explicit stale/polling policy;
- Service Worker disabled does not fail product;
- offline outbox conflict does not mean IndexedDB runtime globally corrupt;
- contractSet mismatch is boot incompatible.
---
## 23. 구현 검증
### 23.1 범위
이 절은 local/unit/component/integration/real-browser behavior를 정의한다. CI stage, workflow, promotion formula는 정의하지 않는다.
### 23.2 Runtime Config/boot cases
```text
valid V2
unknown field reject
API_CONTRACT_VERSION in V2 reject
secret-like key reject
URL credential/hash/query reject
HTTPS rule
config 64KiB exact and +1
manifest 1MiB exact and +1
Content-Length lie
chunked overflow
invalid UTF-8
invalid JSON/media/status
5s timeout
redirect reject
build/release/assets mismatch
contractSet canonical/digest mismatch
fixture excluded from contractSet
partial composition reverse cleanup
retry once/no loop
```
### 23.3 contract package consumer cases
```text
only contribution file imports package
exact version/integrity
packageId/version/digest/source regex
runtime protocol mismatch
package duplicate/mismatch
duplicate operation/event
status sorted unique/subset
path/query/hash bound
validator missing/throw
fixture exact identity/events empty
product local fixture reject
package top-level side-effect fixture reject
```
### 23.4 HTTP cases
```text
request validation before fetch
final invariant after auth patch
request 256KiB/default and hard +1
response/problem byte bounds
redirect/media/status/body policy
required/optional/none body
success/problem validator
scope fence before/after response
safe/idempotent retry
keyed dispatch-loss no retry
never retry
full jitter deterministic source
Retry-After ceiling/deadline
401 no hidden replay
caller/route/scope/deadline cancellation
NOT_STARTED vs MAYBE_APPLIED
inspect recovery projection
```
### 23.5 Router cases
```text
static route registration
lazy module
path/search codec duplicate/unknown/bounds
no direct fetch/QueryClient/adapter import
loader/action server-data reject
focus/scroll/title
route unmount lease cleanup
chunk release/contractSet recovery
reload guard once
dirty navigation/effect-unknown blocker
```
### 23.6 Query/scope cases
```text
one QueryClient
fixed profile registry only
required measureResult
measure throw/non-finite/negative/overflow
item/byte ceiling
scope FENCED synchronous
old snapshot immediately false
cancel/clear/validator/realtime/offline exact ordering
late success discarded
identity bounds/active lease eviction
retry false
memory-only/no persistence
304 with/without current cache
```
### 23.7 Mutation cases
```text
three duplicate policies
same input different scope not joined
join waiter bound
optimistic default none
layer count/single/total bytes
out-of-order commit/rollback
effect unknown no arbitrary rollback
success seed/invalidation ordering
local invalidation failure does not resend
form dirty state clearing only confirmed
```
### 23.8 Cross-context cases
```text
registry bounds/fanout/fanin
no key/data on wire
BroadcastChannel primary
localStorage fallback/storageArea
same-tab local dispatch
duplicate/lower/gap
release/scope epoch
mutation coalescing
overflow full stale
logout/dispose leak
two-page Chromium/Firefox/WebKit semantics
```
### 23.9 Realtime cases
```text
zero contribution no connection/listener
one host/multiple leases
OPEN_UNRESUMED no effect
recovery barrier
500ms/30s/10/5m/60s reconnect policy
offline wait
hidden 30s/no hidden poll
visible recovery
unknown/malformed/schema/mapper/scope
64KiB event +1
queue/dedupe bounds
conflict/gap/expired cursor
single-writer live/poll handoff
effect before checkpoint
checkpoint failure after effect -> recovery
5s subscription grace
StrictMode acquire/release
logout/dispose/bfcache
```
### 23.10 Generic Worker cases
```text
unselected no worker
protocol/version/generation
input/output bounds
queue count/bytes/FIFO
queued cancellation
active cooperative cancellation
250ms terminate
stale result fence
transfer ownership/detached buffer
crash 3/5m then fatal
idle 60s
fallback exact
no network/DOM/credential
```
### 23.11 Service Worker cases
```text
null selection no lookup/register/cache
ACTIVE first-render idempotent registration
module/scope/updateViaCache
ACTIVE+DISABLED exact-owned unregister/cache retain
foreign registration ownership mismatch
ACTIVE -> REMOVE -> PURGE -> null
install all-or-nothing
asset count/item/set/integrity/media
waiting no blind skipWaiting
dirty/effect-unknown activation block
multi-client drain/30s missing client
no clients.claim
first install reload-to-enable
update 6h/manual rate
controllerchange guard
network-only navigation/config/manifest/API
exact static hit before API base root
unknown GET no runtime cache
current+previous retention
owned parser cleanup only
Web Push handler composition
future protocol mismatch
```
### 23.12 Offline/Sync cases
```text
unselected no DB/listener
operation not KEYED/recovery missing reject
package identity/digest
request/count/dataset bounds
transaction complete
lease acquire/renew/release/takeover
LEASED expiry -> PENDING
SENDING expiry -> EFFECT_UNKNOWN
single sender/batch 10/30s
retry/backoff/attempt cap
auth/package/scope fence
ACKED payload delete
retention
unsupported Sync foreground works
Sync event sends zero command
wake message only
crash/reload restore
no credential persistence
removal unresolved command no silent purge
```
### 23.13 browser result vocabulary
```text
SUPPORTED_AND_PASSED
SUPPORTED_AND_FAILED
UNSUPPORTED_WITH_FALLBACK_PASSED
UNSUPPORTED_WITHOUT_REQUIRED_CAPABILITY
ENVIRONMENT_UNAVAILABLE
```
unsupported is not ordinary pass. Background Sync requires foreground fallback pass.
### 23.14 leak inspection
lifecycle suite terminal checks:
```text
active listeners = 0 except declared singleton
active timers = 0
open generic workers = 0
open realtime transports = 0
subscription refs = 0
queued worker/realtime task = 0
active IDB transaction = 0
owned abort controller settled
unhandled rejection = 0
```
persistent Service Worker registration is inspected according to selection mode, not counted as generic leak.
---
## 24. Source 배치와 migration
### 24.1 existing files to modify
```text
src/bootstrap/runtime-config-schema.ts
src/bootstrap/load-runtime-config.ts
src/bootstrap/load-release-manifest.ts
src/bootstrap/create-runtime-composition.ts
src/bootstrap/composition-root.ts
src/bootstrap/runtime-adapters.ts
src/bootstrap/runtime-application.tsx
src/bootstrap/main.tsx
src/contracts/env.ts
src/contracts/release-tokens.ts
src/contracts/storage-keys.ts
src/contracts/api-operations.ts
src/contracts/rest-profiles.ts
src/contracts/server-state.ts
src/contracts/server-state-scope.ts
src/contracts/query-invalidation.ts
src/contracts/realtime-events.ts
src/contracts/realtime-streams.ts
src/adapters/http/bounded-json.ts
src/adapters/http/client.ts
src/adapters/http/request-builder.ts
src/adapters/http/retry-policy.ts
src/adapters/query-cache/server-state-scope-runtime.ts
src/adapters/query-cache/tanstack-cache-coordinator.ts
src/adapters/cross-context-invalidation/browser-cross-context-host.ts
src/adapters/cross-context-invalidation/browser-cross-context-invalidation.ts
src/adapters/cross-context-invalidation/index.ts
src/presentation/adapters/query/application-query.ts
src/presentation/adapters/query/optimistic-layer-runtime.ts
src/presentation/routes/app-router.tsx
src/presentation/routes/navigation-policy.ts
src/presentation/routes/platform-route-codecs.ts
src/presentation/routes/route-codecs.ts
src/presentation/routes/route-contract.ts
src/presentation/routes/route-input.tsx
src/presentation/routes/route-runtime.tsx
src/features/installed-feature-contracts.ts
src/features/installed-feature-adapters.ts
src/features/installed-feature-runtimes.tsx
scripts/generate-build-manifest.ts
vite.config.ts
tsconfig.json
tsconfig.app.json
package.json
.gitignore
eslint.config.ts
vitest.config.ts
playwright.capabilities.config.ts
config/hosting/cache-policy.json
public/config.json
public/release-manifest.json
```
### 24.2 new common files
```text
src/contracts/contract-set.ts
src/contracts/contract-set-canonical.ts
src/contracts/external-contract-runtime.ts
src/contracts/runtime-capabilities.ts
src/contracts/service-worker.ts
src/contracts/web-worker.ts
src/contracts/offline-command.ts
src/bootstrap/read-bounded-boot-json.ts
src/bootstrap/register-service-worker.ts
src/features/installed-contract-contributions.ts
src/features/installed-runtime-capabilities.ts
src/adapters/http/bounded-body-reader.ts
src/adapters/http/http-execution-v3.ts
src/adapters/http/http-effect-certainty.ts
src/adapters/http/http-contract-bridge.ts
src/adapters/platform/browser-lifecycle.ts
scripts/build-frontend.ts
scripts/generate-contract-set.ts
scripts/generate-service-worker-assets.ts
vite.service-worker.config.ts
tsconfig.web-worker.json
tsconfig.service-worker.json
.generated/frontend-runtime/contract-set.ts
.generated/frontend-runtime/service-worker-build-info.ts
.generated/frontend-runtime/service-worker-assets.ts
```
`.generated/frontend-runtime`은 build-time derived source다. 사람이 수정하지 않고 build 시작 시 전체 삭제 후 재생성한다. Git에 commit하지 않는다.
### 24.3 optional source files
Generic Worker selected only:
```text
src/application/ports/web-worker/*
src/adapters/web-worker/*
```
Service Worker active/removal selected:
```text
src/adapters/service-worker/*
```
Offline Commands selected only:
```text
src/application/ports/offline-command/*
src/adapters/offline-command/*
```
### 24.4 feature files
```text
src/features/<feature>/contracts/<service>-contract-contribution.ts
src/features/<feature>/contracts/<feature>-mapper.ts
src/features/<feature>/contracts/<feature>-realtime-contribution.ts
src/features/<feature>/adapters/<feature>-http-gateway.ts
src/features/<feature>/application/<feature>-api.ts
src/features/<feature>/presentation/use-<feature>.ts
```
path token replacement is deterministic.
```text
<feature> = InstalledContractContribution.featureId exactly
lowercase kebab-case regex already validated
<service> = external package name의 slash 뒤 segment
lowercase npm segment 그대로
```
팀이 임의 abbreviation/pluralization을 선택하지 않는다. 파일 충돌이 나면 package contribution을 한 feature 안의 named subdirectory로 분리하되 token 자체를 바꾸지 않는다.
### 24.5 architecture rule amendments
static checks must reject:
```text
@org-contracts import outside feature contract contribution
presentation direct fetch
presentation direct adapters/QueryClient
application React/TanStack/browser API
generic Worker fetch/WebSocket/EventSource
separate Service Worker registration
Service Worker API response cache
runtime deployed spec/registry lookup
product TEMPLATE_FIXTURE
Router loader/action server-data call
```
### 24.6 Runtime Config/manifest migration
```text
RC-1 add V2 schema/parser and bounded reader
RC-2 support V1/V2 reader, current writer V1
RC-3 generate contractSet and writer V2
RC-4 ignore accepted V1 scalar only in V1 input
RC-5 all fixtures/public files V2
RC-6 remove V1 parser/scalar after compatibility window
```
### 24.7 HTTP migration
```text
HTTP-1 normalize current local reference fixture into contribution
HTTP-2 introduce external package contribution interface
HTTP-3 bridge current ApiOperationV2 reader
HTTP-4 add execution V3/outcome/certainty
HTTP-5 move feature gateway one operation at a time
HTTP-6 remove duplicate local path/status/type
HTTP-7 retire V2 writer after all installed operations V3
```
### 24.8 Query migration
```text
Q-1 fixed profile registry
Q-2 required measureResult for current reference definitions
Q-3 scope lifecycle FENCED/READY
Q-4 commit fence/result budget
Q-5 many-to-many invalidation registry
Q-6 mutation duplicate/optimistic exact policies
```
### 24.9 Realtime migration
```text
RT-1 keep common source unchanged where possible
RT-2 external event contract bridge
RT-3 feature effect contribution
RT-4 optional runtime host integration
RT-5 no product selection -> no bootstrap composition
```
### 24.10 Service Worker migration/removal
first adoption:
```text
SW-1 protocol/build-info virtual module
SW-2 physical entry/handler composition
SW-3 static asset manifest/cache
SW-4 page controller/waiting/drain
SW-5 first-install/update lifecycle
```
removal:
```text
Release N mode ACTIVE
Release N+1 mode REMOVE_REGISTRATION
Release N+2 mode PURGE_OWNED_RESOURCES after rollback window
Release N+3 null
```
### 24.11 optional source zero-side-effect
optional source may exist as `AVAILABLE_NOT_COMPOSED`, but production bootstrap import is absent unless static selection references it.
Vite module inventory must show:
- unselected worker entry absent;
- unselected Service Worker active entry absent;
- unselected offline adapter absent from production runtime graph where tree-shaking/static import boundary permits;
- recipe/test source is not production import.
### 24.12 sample/reference removal
removing `src/features/reference-feature` must not remove common runtime.
removal sequence:
```text
reference feature directory
-> reference template contribution
-> reference route/messages/runtime registration
-> reference tests
-> no remaining import/string in production bundle
```
common external contract consumer interfaces remain.
---
## 25. 구현 순서와 stop condition
### 25.1 principle
각 phase는 independently reviewable runtime increment다. API/backend contract authoring과 CI workflow는 이 순서에 포함하지 않는다.
### Phase A — authority cleanup
1. old generic design을 non-authoritative로 표시;
2. this document reference add;
3. external package ownership boundary document;
4. current source path inventory freeze.
stop condition:
- current repository snapshot/path가 문서와 다르면 후속 code 금지;
- contract package format이 library design과 다르면 contribution interface부터 amend.
### Phase B — contract package consumer foundation
1. `RuntimeValidator` adapter;
2. normalized contribution types;
3. installed contribution composer;
4. fixture vs external provenance;
5. validation/duplicate checks;
6. reference fixture normalization.
exit:
- package import one path;
- product fixture prohibited;
- no runtime network/package lookup.
### Phase C — contractSet and boot V2
1. canonical bytes/digest;
2. Release Manifest V2 schema;
3. bounded boot reader;
4. Runtime Config V2;
5. contractSet boot check;
6. partial cleanup stack;
7. public fixture migration.
exit:
- scalar API contract version no longer writer source;
- boot body bounded/deadline;
- product mount gated by contractSet.
### Phase D — HTTP execution V3
1. request/response hard ceilings;
2. package descriptor bridge;
3. final invariant after credential patch;
4. outcome/failure union;
5. total deadline/retry owner;
6. effect certainty;
7. current reference operation migration.
exit:
- direct/duplicate path/status schema removed for migrated operation;
- keyed uncertain command never automatic replayed.
### Phase E — Router/Query/Mutation lifecycle
1. Router no-server-data rule;
2. fixed Query profiles;
3. required measureResult;
4. scope FENCED/READY exact ordering;
5. commit fence;
6. invalidation many-to-many;
7. duplicate mutation coordinator;
8. bounded optimistic layers.
exit:
- old scope late result cannot commit;
- Query/Mutation retry false;
- local invalidation failure cannot resend command.
### Phase F — Cross-context
1. registry bounds;
2. protocol/epoch envelope;
3. BroadcastChannel;
4. localStorage fallback;
5. gap/full-stale;
6. scope/logout lifecycle.
exit:
- no data/key on wire;
- two-page semantics;
- disposal leak zero.
### Phase G — Realtime product bridge
1. external event validator bridge;
2. installed realtime contribution;
3. host lifecycle;
4. subscription lease bridge;
5. recovery/effect/checkpoint ordering;
6. no-selection zero side effect.
stop condition:
- no exact endpoint/event/recovery contribution -> remain `NOT_SELECTED`;
- common runtime is not enough to compose.
### Phase H — Generic Worker optional
execute only when a selected task exists.
1. task port/protocol;
2. group worker runtime;
3. queue/cancel/crash;
4. fallback;
5. browser behavior.
stop condition:
- no profiling/owner/task contribution -> do not implement production entry.
### Phase I — Service Worker reference runtime
1. static selection/removal mode;
2. one physical entry;
3. protocol/registration;
4. static cache;
5. waiting/client drain;
6. first install/update;
7. runtime disabled unregister;
8. removal cleanup.
exit:
- no API/navigation cache;
- no blind activation/claim;
- persistent removal path proven.
### Phase J — Offline Command optional
execute only when product operation is selected.
1. operation allowlist/record codec;
2. IDB repository;
3. foreground sender lease;
4. state transitions;
5. effect unknown recovery;
6. retention/summary;
7. optional Sync wake handler;
8. removal.
stop condition:
- no KEYED+recovery package descriptor -> remain `NOT_SELECTED`;
- no product conflict/recovery UX -> do not compose.
### Phase K — final integration review
1. lifecycle shutdown/bfcache;
2. security/redaction;
3. all behavior cases;
4. source/path/document consistency;
5. old compatibility reader removal only after no caller.
---
## 26. 완료 기준
### 26.1 common
- current repository path/architecture retained;
- no parallel platform layer;
- no backend/API/event source ownership duplication;
- no CI design included;
- optional unselected side effect zero;
- exact lifecycle/disposal;
- bounded input/output/queue/deadline;
- typed failure and uncertainty;
- current scope fence;
- secret/privacy rules;
- source/removal path.
### 26.2 external contract consumer
- exact immutable package pin;
- one import location;
- fixture/product provenance separated;
- normalized descriptor/validator;
- package validation bounds;
- local product schema duplication absent;
- runtime registry/deployed docs lookup absent;
- mapper boundary.
### 26.3 contractSet/boot
- V2 manifest;
- canonical length-prefixed digest;
- fixture excluded;
- Runtime Config scalar removed;
- 64KiB/1MiB/5s bounded read;
- strict URL/media/JSON/schema;
- reverse partial cleanup;
- safe retry once.
### 26.4 HTTP
- package descriptor-driven;
- request/response bounds;
- final invariant after auth;
- redirect reject;
- total deadline;
- transport-only retry;
- body/status/media validation;
- effect certainty;
- keyed uncertainty no automatic resend;
- scope fence.
### 26.5 Router
- static route/codec;
- no server-data loader/action;
- no direct fetch/cache;
- lazy/focus/scroll/error;
- chunk contractSet recovery;
- dirty/effect-unknown blocker.
### 26.6 Query/Mutation
- one QueryClient;
- fixed profiles;
- required result measurement;
- scope exact reset order;
- late result discard;
- memory-only;
- bounded identity/invalidation;
- duplicate coordinator;
- optimistic default none/bounded layer;
- success/failure ordering.
### 26.7 Realtime
- existing common runtime reused;
- product contribution exact;
- open/resume separated;
- reconnect policy exact;
- hidden/offline lifecycle;
- validation pipeline;
- queue/dedupe bounds;
- gap recovery;
- effect-before-checkpoint;
- ref-counted cleanup;
- zero selection side effect.
### 26.8 Generic Worker
- explicit selected task;
- semantic port;
- one worker per group;
- protocol validation;
- queue/input/output/deadline;
- cancellation/terminate;
- crash budget;
- stale fence;
- no network/credential;
- fallback exact.
### 26.9 Service Worker
- one physical registration;
- post-mount module registration;
- build/protocol handshake;
- all-or-nothing static cache;
- navigation/API/config/manifest network-only;
- waiting/client drain;
- no blind skipWaiting/clients.claim;
- first install reload-to-enable;
- runtime disabled exact unregister;
- ACTIVE→REMOVE→PURGE→null;
- foreign resource protection.
### 26.10 Offline/Background Sync
- product operation selected;
- external KEYED/recovery descriptor;
- IDB transaction-complete;
- exact bounds;
- single sender lease;
- SENDING crash -> EFFECT_UNKNOWN;
- no automatic unknown resend;
- foreground fallback;
- Sync wake only;
- retention/recovery/removal.
---
## 27. 위험 등록부
| ID | 위험 | 차단 결정 | 잔여 위험 |
| --- | --- | --- | --- |
| FRT-R001 | frontend local API schema가 두 번째 SSOT | external package one import/provenance | package publication quality는 external owner |
| FRT-R002 | scalar contract version으로 multi-service mismatch | contractSet V2 | provider compatibility는 external release system |
| FRT-R003 | boot body memory amplification | streaming byte bound/deadline | browser/network implementation variance |
| FRT-R004 | auth patch가 URL/method/header 변조 | final invariant recheck | external auth adapter defect |
| FRT-R005 | duplicate retry amplification | HTTP-only retry owner | user repeated intent remains product UX |
| FRT-R006 | command timeout duplicate effect | MAYBE_APPLIED/inspect/no resend | server inspect may remain unavailable |
| FRT-R007 | Router/Query dual cache | loader server-data prohibition | future SSR requires separate design |
| FRT-R008 | old account late result leak | synchronous scope FENCED/commit fence | third-party widget outside scope |
| FRT-R009 | result memory blowup | required measure/fixed profiles | feature estimator defect |
| FRT-R010 | cross-tab stale data replication | hint-only protocol | hints can be lost; refetch recovery |
| FRT-R011 | reconnect storm | full jitter/attempt/episode bounds | provider-wide outage |
| FRT-R012 | connection open mistaken as recovered | OPEN_UNRESUMED/barrier | server recovery semantics external |
| FRT-R013 | malformed event corrupts state | validator/mapper/gap recovery | malicious volume before edge controls |
| FRT-R014 | subscription leak under StrictMode | ref-count + 5s grace | browser process crash |
| FRT-R015 | Worker queue/crash heap amplification | exact bounds/restart budget | heavy task still impacts CPU/battery |
| FRT-R016 | multiple Service Worker registrations | one physical entry | existing foreign registration conflict |
| FRT-R017 | stale worker mixed with new page | drain/no claim/reload handshake | user may postpone update |
| FRT-R018 | service worker removed from source but persists | staged removal modes | clients not revisiting app remain registered |
| FRT-R019 | Cache Storage poisoning/stale HTML | static manifest only/no navigation cache | immutable asset host misconfiguration |
| FRT-R020 | offline command duplicate after crash | SENDING->EFFECT_UNKNOWN | external reconciliation may need human |
| FRT-R021 | Background Sync assumed reliable | wake-only/foreground fallback | browser may throttle background work |
| FRT-R022 | logout leaves old durable data visible | scope partition fence | physical cleanup may be delayed |
| FRT-R023 | diagnostics leak | closed allowlist/redaction | vendor SDK internal behavior |
| FRT-R024 | bfcache resumes stale release | manifest refresh/recovery | offline manifest fetch unavailable |
| FRT-R025 | over-design installs optional capability unused | static selection/stop conditions | future teams may bypass process |
---
## 28. Normative decision ledger
| Decision ID | 결정 | 정본 |
| --- | --- | --- |
| FRT-D001 | 기존 repository layers 유지 | §1.1–§1.2 |
| FRT-D002 | backend/API/event source design 제외 | §0.3 |
| FRT-D003 | CI design 제외 | §0.4 |
| FRT-D004 | immutable external package only | §4 |
| FRT-D005 | package import one feature contribution path | §4.1 |
| FRT-D006 | template fixture HTTP-only and contractSet excluded | §4.8–§4.9 |
| FRT-D007 | scalar API contract version removed | §5.1 |
| FRT-D008 | length-prefixed contractSet canonical digest | §5.3 |
| FRT-D009 | Runtime Config may disable, never enable source-absent capability | §3.5, §6.1 |
| FRT-D010 | boot 64KiB/1MiB/5s | §6.4–§6.5 |
| FRT-D011 | boot reverse cleanup | §6.8 |
| FRT-D012 | descriptor-driven HTTP execution | §7.1–§7.2 |
| FRT-D013 | request/response hard bounds | §7.3 |
| FRT-D014 | transport-only retry | §8.1 |
| FRT-D015 | 250ms/2s/full-jitter/max2 HTTP retry | §8.2 |
| FRT-D016 | 401 no hidden replay | §8.4 |
| FRT-D017 | dispatch loss MAYBE_APPLIED | §8.7–§8.9 |
| FRT-D018 | Router server-data loader/action prohibited | §9.2 |
| FRT-D019 | one QueryClient | §10.1 |
| FRT-D020 | four fixed Query profiles | §10.2 |
| FRT-D021 | required feature-owned measureResult, no fallback | §10.3–§10.4 |
| FRT-D022 | synchronous scope FENCED exact order | §10.5–§10.6 |
| FRT-D023 | Query persistence disabled | §10.10 |
| FRT-D024 | mutation duplicate default reject | §11.2 |
| FRT-D025 | optimistic default none, layer bounds | §11.4–§11.5 |
| FRT-D026 | cross-tab hint-only, many-to-many bounds | §12 |
| FRT-D027 | existing realtime runtime reused | §13.1 |
| FRT-D028 | product realtime explicit contribution | §13.2–§13.6 |
| FRT-D029 | single writer live XOR polling | §13.8 |
| FRT-D030 | OPEN_UNRESUMED separate | §14.1–§14.2 |
| FRT-D031 | realtime reconnect 500ms/30s/10/5m/60s | §14.3 |
| FRT-D032 | hidden 30s then close, no hidden polling | §14.5 |
| FRT-D033 | event hard limits reuse current constants | §15.1 |
| FRT-D034 | effect before checkpoint | §15.7 |
| FRT-D035 | subscription 5s grace, scope immediate | §15.8–§15.9 |
| FRT-D036 | generic Worker default not selected | §16.2 |
| FRT-D037 | one Worker per group, lazy/60s idle | §16.5 |
| FRT-D038 | Worker queue/cancel/crash exact policy | §16.8–§16.10 |
| FRT-D039 | Worker no network/credential | §16.13 |
| FRT-D040 | one physical Service Worker | §17.1 |
| FRT-D041 | Service Worker after first committed render | §17.5 |
| FRT-D042 | ACTIVE/REMOVE/PURGE/null lifecycle | §17.3 |
| FRT-D043 | disabled exact-owned unregister, cache retained | §17.6 |
| FRT-D044 | no blind skipWaiting, no clients.claim | §17.10–§17.12 |
| FRT-D045 | static asset cache only | §18 |
| FRT-D046 | navigation/API/config/manifest network-only | §18.5–§18.8 |
| FRT-D047 | Offline queue selected only with KEYED recovery package | §19.3 |
| FRT-D048 | offline records 1000/50MiB/256KiB | §19.5 |
| FRT-D049 | sender lease 30s/renew10s/batch10/30s | §19.9 |
| FRT-D050 | SENDING crash -> EFFECT_UNKNOWN | §19.12 |
| FRT-D051 | Background Sync wake-only | §19.18 |
| FRT-D052 | lifecycle shutdown exact order | §20.3 |
| FRT-D053 | no unload correctness | §20.6 |
| FRT-D054 | closed diagnostics allowlist | §22 |
| FRT-D055 | behavior verification separate from CI | §23 |
| FRT-D056 | implementation phase stop conditions | §25 |
---
## 부록 A. Numeric constants
### A.1 Boot
| Constant | Value |
| --- | ---: |
| Runtime Config bytes | 65,536 |
| Release Manifest bytes | 1,048,576 |
| each boot request deadline | 5,000ms |
| boot retry count | 1 |
| manifest URL bytes | 256 |
### A.2 HTTP
| Constant | Value |
| --- | ---: |
| default request | 262,144 bytes |
| hard request | 1,048,576 bytes |
| default response | 1,048,576 bytes |
| hard response | 8,388,608 bytes |
| problem body | 65,536 bytes |
| query string | 8,192 bytes |
| final URL | 16,384 bytes |
| default deadline | 10,000ms |
| hard deadline | 60,000ms |
| retry base | 250ms |
| retry local max | 2,000ms |
| Retry-After max | 5,000ms |
| retries | 2 |
### A.3 Query
| Profile | stale | gc | items | bytes |
| --- | ---: | ---: | ---: | ---: |
| DETAIL_STANDARD | 30s | 5m | 1 | 256KiB |
| LIST_STANDARD | 15s | 5m | 200 | 1MiB |
| LOOKUP_STABLE | 5m | 30m | 500 | 2MiB |
| VOLATILE_STATUS | 0 | 1m | 1 | 64KiB |
identity registry:
| Constant | Value |
| --- | ---: |
| depth | 12 |
| nodes | 512 |
| string bytes | 2,048 |
| identity bytes | 16,384 |
| entries | 4,096 |
| canonical memory | 4MiB |
### A.4 Invalidation
| Constant | Value |
| --- | ---: |
| topics | 256 |
| namespaces | 256 |
| edges | 1,024 |
| topic fan-out | 64 |
| namespace fan-in | 64 |
| pulse bytes | 1,024 |
| pulse TTL | 60s |
### A.5 Realtime
| Constant | Value |
| --- | ---: |
| event bytes | 64KiB |
| payload depth | 16 |
| payload nodes | 4,096 |
| queue events | 256 |
| queue bytes | 4MiB |
| dedupe entries | 2,048 |
| dedupe bytes | 4MiB |
| dedupe TTL | 10m |
| reconnect base | 500ms |
| reconnect max | 30s |
| attempts | 10 |
| episode | 5m |
| stable reset | 60s |
| hidden grace | 30s |
| subscription grace | 5s |
| stream drain | 2s |
### A.6 Web Worker
| Constant | Value |
| --- | ---: |
| groups | 16 |
| tasks/group | 32 |
| active groups | 4 |
| queued/group | 32 |
| queued bytes/group | 16MiB |
| default input/output | 1MiB |
| hard input/output | 8MiB |
| default deadline | 5s |
| hard deadline | 30s |
| cancel grace | 250ms |
| idle terminate | 60s |
| restarts | 3/5m |
| transferables | 16 |
### A.7 Service Worker/static cache
| Constant | Value |
| --- | ---: |
| assets | 256 |
| single asset | 2MiB |
| set | 5MiB |
| fetch concurrency | 4 |
| install deadline | 60s |
| client drain | 30s |
| update check | 6h |
| retained previous cache | 1 |
### A.8 Offline Commands
| Constant | Value |
| --- | ---: |
| operations | 64 |
| default request | 256KiB |
| hard request | 1MiB |
| records | 1,000 |
| dataset | 50MiB |
| sender lease | 30s |
| renew | 10s |
| batch | 10 |
| batch window | 30s |
| parallel send | 1 |
| retry base | 1s |
| retry max | 5m |
| attempts | 10 |
| ordinary retention | 7d |
| conflict/unknown | 30d |
| Sync re-register minimum | 1m |
---
## 부록 B. 금지 구현 목록
```text
create src/platform/* parallel runtime
copy OpenAPI/AsyncAPI/JSON Schema into product frontend
import backend Java DTO/module
fetch deployed /v3/api-docs during build/runtime
resolve Schema Registry latest at runtime
use mutable package tag/range
let component/hook call raw fetch/socket/Worker/IndexedDB
let Router loader create server cache or fetch
enable TanStack retry while HTTP transport retries
retry keyed command after dispatched response loss
interpret abort as server rollback
store generated wire DTO directly in Query cache
use JSON.stringify as generic Query result size fallback
persist Query payload in Web Storage by default
broadcast Query key/data/input/cursor
open one realtime connection per component
apply event before validation/scope/gap checks
write checkpoint before effect
continue applying deltas after gap/overflow
use hidden polling automatically
create generic Worker for network I/O
allow Worker credential/fetch/WebSocket
create Blob/data URL Worker
register PWA and Web Push workers separately for same scope
call skipWaiting without client drain
call clients.claim in baseline
cache index.html navigation fallback
cache API/private/auth responses in Cache Storage
treat unregister as cache deletion
switch Service Worker ACTIVE directly to null
unregister foreign registration by scope prefix guess
send authenticated command from baseline Background Sync
reset expired SENDING record to PENDING
manual resend EFFECT_UNKNOWN without inspect
store token/URL/header/cursor in diagnostics
use unload/sendBeacon as correctness boundary
claim unsupported browser capability as pass
claim COMPOSED means production/provider-ready
```
---
## 부록 C. 플랫폼 참고 기준
구현 시 pinned browser/library version의 공식 문서를 다시 확인하되 본 문서의 repository decision을 임의 변경하지 않는다.
- Fetch Standard: <https://fetch.spec.whatwg.org/>
- HTML — Web Workers: <https://html.spec.whatwg.org/multipage/workers.html>
- WebSockets Standard: <https://websockets.spec.whatwg.org/>
- HTML — Server-sent events: <https://html.spec.whatwg.org/multipage/server-sent-events.html>
- Service Workers: <https://w3c.github.io/ServiceWorker/>
- ServiceWorkerContainer.register: <https://developer.mozilla.org/docs/Web/API/ServiceWorkerContainer/register>
- Background Synchronization: <https://wicg.github.io/background-sync/spec/>
- SyncManager: <https://developer.mozilla.org/docs/Web/API/SyncManager>
- Cache API: <https://developer.mozilla.org/docs/Web/API/Cache>
- IndexedDB: <https://w3c.github.io/IndexedDB/>
- TanStack Query v5: <https://tanstack.com/query/v5/docs/framework/react/overview>
- React Router: <https://reactrouter.com/>
- React `useSyncExternalStore`: <https://react.dev/reference/react/useSyncExternalStore>
---
## 최종 판정
본 문서는 프론트엔드 저장소가 소유하지 않는 backend/API/event 계약 authoring과 CI orchestration을 제거하고, 현재 코드 구조 안에서 contract package 소비부터 HTTP, Router, Query/Mutation, realtime, Worker, Service Worker와 offline lifecycle까지 구현 결정을 닫는다.
구현자는 다음을 다시 선택하지 않는다.
```text
source root
contract import boundary
contractSet format/digest
Runtime Config V2 shape
boot byte/deadline
HTTP retry owner/limits
mutation uncertainty
Router data ownership
Query profiles/result measurement
scope reset order
cross-tab wire
realtime reconnect/resume/queue
subscription cleanup
Worker topology/bounds
Service Worker registration/update/removal/cache policy
offline sender/state/retention
Background Sync role
shutdown order
```
외부 contract package가 필요한 의미를 제공하지 않으면 frontend가 가정으로 채우지 않는다. 해당 product contribution은 `NOT_SELECTED` 또는 `INCOMPATIBLE` 상태로 남기며, common runtime 구현은 계속 독립적으로 검증 가능해야 한다.