5958 lines
154 KiB
Markdown
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 구현은 계속 독립적으로 검증 가능해야 한다.
|