R-02: add an OPEN/DRAINING/CLOSED lifecycle orthogonal to freshness. Effect and recovery authorities are now awaited under a deadline: on expiry the commit capability is revoked and the work aborted, the caller gets a bounded non-retryable IDLE_TIMEOUT, and the underlying task is retained rather than dropped. A draining stream refuses new events and recovery, and close() returns a Promise that succeeds only once every retained task actually settled, reporting IDLE_TIMEOUT otherwise. R-03: a handoff fail-close moves active, probe, quiescing and transition leases into a retired-writer set before clearing their references, and close() waits on current and retired writers together, so an abandoned non-cooperative writer can no longer make teardown report a false success. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
108 KiB
Realtime events, Web Push, and bounded polling
정본 안내 (non-authoritative for runtime capability decisions)
Runtime Config/boot, Fetch HTTP client, Router, Query/Mutation, realtime 공통 경계, Web Worker, Service Worker, offline command와 Background Sync의 구현 결정은 프론트엔드 런타임 Capability 저장소 정합형 구현 결정 폐쇄 상세 설계가 정본이다. 본 문서의 해당 서술이 정본과 충돌하면 정본을 따른다.
이 문서는 SSE, WebSocket, Web Push와 제한된 Polling을 frontend에 도입할 때의 선택 기준, 포트·어댑터 경계, event 계약, 재연결·복구, 인증, 브라우저 lifecycle, 관측성과 promotion 조건을 정의한다.
이 네 기술은 모두 “새 정보가 도착했음을 frontend에 알린다”는 점만 비슷하다. 연결 방향, 실행 가능한 browser 상태, 전달 보장과 실패 복구가 다르므로 하나의 범용 realtime transport로 합치지 않는다.
현재 저장소에는 production에서 제외된 RT-01~RT-04 reference runtime과
deterministic contract/fault test가 있다. transport-independent event authority,
bounded reconnect owner, single-writer live↔Poll handoff, fetch-stream SSE,
bounded Polling, closed WebSocket protocol, Web Push window/Service Worker
adapter까지 AVAILABLE_NOT_COMPOSED이지만 제품별 endpoint,
event/query/notification registry, backend replay/provider protocol과 production
composition은 선택하지 않았다. 따라서 reference source가 있다는 사실은
capability가 설치됐거나 production-ready라는 뜻이 아니다.
상세 결정은 VD-28이 소유한다. 공통 cache와 account scope는 Client cache and browser storage platform, 선택 capability의 설치·제거 절차는 Optional frontend adapter recipes, 계층 방향은 Frontend ports, adapters, and boundaries 를 따른다.
Bounded task lease와 DRAINING (R-02, R-03)
non-cooperative effect/recovery authority 하나가 stream tail 전체를 영구
wedge하지 못하도록, common coordinator는 각 task를 deadline으로 감싼다. deadline
초과 시 commit capability는 즉시 취소되지만 task 자체는 retainedTasks에 남아
stream을 DRAINING으로 유지한다. close()는 이 retain 집합이 실제로 settle해야
성공을 반환한다. handoff coordinator도 같은 원칙으로 fail-close된 writer를
retiredWriters에 보존한다.
0. 현재 상태와 목표 delta
이 문서에서 설계 승인, reference source 존재, production 조합과 target browser의 보장은 서로 다른 사실이다. primary current status는 기존 browser capability 문서와 같은 다섯 literal만 사용한다.
| 상태 | 의미 |
|---|---|
COMPOSED |
production bootstrap 또는 설치된 feature 경로가 concrete runtime을 생성하고 소비한다. |
AVAILABLE_NOT_COMPOSED |
reusable runtime과 deterministic/native test가 있으나 production graph에는 없다. |
DESIGNED_NOT_IMPLEMENTED |
경계와 불변조건은 승인됐지만 필요한 실행 source 또는 orchestration이 없다. |
NOT_SELECTED |
제품 요구, owner, backend 계약과 운영 정책이 승인되지 않았다. |
PLATFORM_LIMITED |
browser/platform이 요구한 의미를 공통으로 보장할 수 없다. |
현재 catalog의 RECIPE_AVAILABLE은 위 runtime 상태와 다른 축이다. realtime
row의 referenceRuntime.status=AVAILABLE_NOT_COMPOSED가 reusable source의
존재를 별도로 기록하며, 제품 선택이나 production 조합을 뜻하지 않는다.
| capability | primary current status | 현재 있는 것 | 목표 또는 잔여 |
|---|---|---|---|
| common event/recovery runtime | AVAILABLE_NOT_COMPOSED |
scope/generation fence, effect-after-checkpoint, exact recovery barrier와 deterministic fault test | 제품 stream/event/effect/recovery registry |
| reconnect와 live↔Poll ownership | AVAILABLE_NOT_COMPOSED |
finite full-jitter reconnect owner, authoritative close classification, single-writer handoff와 bounded quiescence/checkpoint | 제품 lifecycle/fallback policy와 transport composition |
| SSE reference runtime | AVAILABLE_NOT_COMPOSED |
bounded fetch-stream adapter/parser, fixed same-origin endpoint, resume/heartbeat/reconnect와 deterministic fault test | actual local streaming server와 target-browser evidence |
| WebSocket reference runtime | AVAILABLE_NOT_COMPOSED |
exact realtime.v1 protocol, bounded queue/rate/buffer, resume/recovery와 deterministic facade test |
actual local server load와 target-browser evidence |
| Web Push subscription/window runtime | AVAILABLE_NOT_COMPOSED |
permission/subscription control, backend registration/revoke facade와 durable association fence | actual provider, permission UX와 target-browser evidence |
| Service Worker push inbound runtime | AVAILABLE_NOT_COMPOSED |
strict hint/click codec, notification/route registry와 uncomposed worker runtime factory | selected worker composition과 provider/browser evidence |
| bounded polling coordinator | AVAILABLE_NOT_COMPOSED |
single-flight, visible/online finite lease, elapsed/attempt budget와 terminal stop test | 제품 operation registry와 freshness policy |
| 제품 realtime delivery | NOT_SELECTED |
owner, endpoint, event registry, SLO가 없음 | 측정된 freshness 요구와 backend/provider 계약 승인 후 선택 |
| 제품 Web Push | NOT_SELECTED |
permission copy, subscription owner, notification policy가 없음 | user-visible background notification 요구가 승인될 때 별도 선택 |
| exactly-once delivery | PLATFORM_LIMITED |
없음 | 공통 목표로 선언하지 않음; duplicate-tolerant 처리와 authoritative resync 사용 |
| global event ordering | PLATFORM_LIMITED |
없음 | V1은 logical stream-local ordering만 계약 |
| always-on background SSE/WebSocket/Polling | PLATFORM_LIMITED |
없음 | hidden/frozen/terminated browser 상태에서 공통 보장하지 않음 |
| timely cross-browser Web Push | PLATFORM_LIMITED |
없음 | best-effort notification과 foreground authoritative refresh만 제공 |
별도로 realtime catalog row 자체는 RECIPE_AVAILABLE이다. 이것은 primary
runtime status가 아니다. 기존 copyable RealtimePort를 production wire
authority나 generic mega port로 확장하지 않았고, reference runtime의 공개 계약은
RealtimeEventAuthority, WebPushControlPort와 transport-specific factory로
나뉜다. 이 source는 production graph 밖에 있다.
설계 승인 직후 canonical readiness 기본값은 다음과 같다.
Selection = NOT_SELECTED
TrafficAdmission = DISABLED
RuntimeHealth = UNKNOWN
PromotionEvidence = MISSING
PromotionEvidence는 문서가 존재한다는 이유로 PARTIAL이나 COMPLETE가 되지
않는다. 실제 frontend contract, backend/provider conformance, target-browser
evidence와 operations drill이 있어야 별도로 승격한다.
현재 recipe 계약의 channel: string, sequence: number, 단일 resumeToken,
callback과 application-facing heartbeat()는 production wire authority가 아니다.
scope/epoch, closed event type, gap, byte limit, 진행되는 cursor, connection
generation과 effect certainty가 없기 때문이다. 선택 시 recipe를 그대로 import하지
않고 이 문서와 실제 backend protocol에 맞게 더 좁은 계약으로 이동한다.
이 문서의 최상위 불변조건은 다음과 같다.
연결이 열려 있음, event 수신, Web Push 수신 또는 Polling 성공은 최신 상태, authorization, server commit이나 정확히 한 번의 처리를 각각 보장하지 않는다. 일반 server state의 source of truth는 계속 서버이며 gap과 불확실성은 authoritative snapshot으로 복구한다.
1. 변경할 수 없는 설계 결정
1.1 하나의 범용 RealtimePort로 합치지 않는다
foreground one-way notification
-> SSE connection owner
-> validated event inbound adapter
foreground duplex interaction
-> WebSocket protocol owner
-> bounded send/receive coordinators
-> validated event/ack inbound adapters
inactive-browser notification
-> backend Web Push provider
-> browser push service
-> Service Worker inbound adapter
-> notification or bounded foreground handoff
relaxed freshness or explicit fallback
-> bounded polling scheduling policy
-> existing HTTP/query gateway
transport: "SSE" | "WEBSOCKET" | "PUSH" | "POLLING" 하나를 받는 facade는
다음 차이를 숨긴다.
- SSE는 active document의 server-to-client UTF-8 stream이다.
- WebSocket은 application message를 양방향으로 교환하지만 browser API가 incoming backpressure를 제공하지 않는다.
- Web Push는 push service와 Service Worker, 사용자 permission, server-side subscription registry가 필요한 background notification capability다.
- Polling은 새 network transport가 아니라 기존 HTTP operation을 실행하는 제한된 scheduling policy다.
- SSE/WS의 cursor와 Web Push subscription endpoint는 수명·민감도·authority가 다르다.
unsubscribe, socket close, push subscription revoke와 poll lease 종료는 같은 effect가 아니다.
공통으로 재사용할 수 있는 것은 closed event envelope 검증, scope generation fence, dedupe/gap detection, backoff 계산, redacted observation과 authoritative resync orchestration이다. native constructor와 lifecycle owner는 분리한다.
1.2 source of truth와 delivery 의미
- 일반 server entity/collection의 source of truth는 서버다.
- SSE/WebSocket event의 기본 역할은 “어떤 namespace가 바뀌었을 수 있다”는 versioned hint다.
- event payload를 TanStack Query cache나 domain entity에 자동으로 덮어쓰지 않는다. 기본 효과는 validated event → registered invalidation topic → authorization을 다시 통과한 HTTP refetch다.
- 제품이 authoritative delta 적용을 선택하려면 server revision, base revision, atomic commit point, gap recovery와 conflict semantics를 event type별로 증명해야 한다.
- Web Push payload는 사용자에게 알릴 작은 opaque hint다. live state delta, ordered stream, read receipt 또는 background synchronization authority가 아니다.
- Polling의
200/304는 해당 HTTP representation의 결과다. 다른 stream의 event 처리나 background notification 성공을 증명하지 않는다. CURSORprofile의 resume cursor는 replay 위치다. credential, authorization proof, entity revision, event ID 또는 global ordering과 동일시하지 않는다.- transport가 확인한 delivery는 application effect commit을 뜻하지 않는다.
browser 종료, replay retention 만료, SNAPSHOT_ONLY stream과 Push
expiry/permission/provider 상태까지 포함한 공통 delivery 보장은 없다. 다만
retention 안의 CURSOR foreground profile에서 server가 전달·replay한 event의
처리 모델은 duplicate-tolerant at-least-once다. application 효과는
idempotent하거나 authoritative refetch로 수렴해야 한다. 그 밖의 profile은
best-effort이며 snapshot만 최신 상태의 authority다. exactly-once는 공통 목표가
아니다.
1.3 inbound와 outbound 경계를 분리한다
연결을 만들고 닫는 책임은 outbound infrastructure다.
- fixed endpoint 선택
- session credential 협력
- connect, subscribe, resume와 reconnect
- WebSocket control frame과 선택된 typed command 송신
- push subscription 생성·backend 등록·해제
- poll request scheduling과 cancellation
외부 event를 application 의도로 바꾸는 책임은 inbound adapter다.
- raw bytes/frame의 hard cap
- UTF-8/JSON/wire schema/version 검증
- scope, stream epoch, event type와 generation 확인
- duplicate/out-of-order/gap 처리
- feature event input 또는 query invalidation effect로 mapping
- 처리 결과 뒤 cursor/ack commit
물리적으로 하나의 runtime factory가 두 역할을 조립할 수는 있지만 public
interface와 module dependency는 분리한다. application/domain에는 EventSource,
WebSocket, MessageEvent, PushSubscription, ServiceWorkerRegistration,
native Notification, URL/header, TanStack 또는 vendor SDK type을 노출하지
않는다.
WebSocket send가 필요해도 send(payload: unknown)을 application port로 만들지
않는다. PublishPresence, AcknowledgeAssignment처럼 실제 업무 capability와
closed payload를 feature가 소유한다. durable mutation은 별도 선택이 없으면
기존 HTTP/idempotency 경로를 유지한다.
1.4 공통 mechanism과 제품 정책을 분리한다
공통 runtime이 소유한다.
- strict protocol/envelope parser와 byte/depth/count ceiling
- immutable config snapshot과 fixed endpoint lookup
- scope/generation fence
- sequential apply queue, bounded dedupe와 gap detection
- cursor commit ordering과 reset orchestration
- backoff/jitter, retry budget와 timer cleanup
- online/visibility/page lifecycle adapter
- native exception과 close reason redaction
- terminal/idempotent
close()/dispose()
composition 또는 feature owner가 결정한다.
- freshness SLO와 transport 선택
- event stream/topic/type registry
- event payload codec와 application effect
- account/tenant/session scope projection
- replay retention, snapshot endpoint와 cursor reset UX
- event rate/size, connection/subscription와 polling budget
- hidden 상태의 grace/close 정책
- push permission copy, notification content와 retention
- backend/provider, rollout과 kill switch
backend/provider가 소유한다.
- connection, subscription과 각 command의 authorization
- commit 이후 event publication과 replay ledger
- stream epoch, sequence, cursor retention과 reset response
- SSE proxy flush/idle contract
- WebSocket upgrade, connection/rate/backpressure limit
- push subscription registry, encryption, VAPID private key와 delivery cleanup
- polling snapshot/ETag/rate-limit semantics
제품 owner와 protocol authority가 없는 skeleton은 임의 topic, endpoint, notification text나 poll interval을 자동 등록하지 않는다.
2. Transport 선택
2.1 선택표
| 요구 | 우선 후보 | 선택 조건 | 선택하지 않는 조건 |
|---|---|---|---|
| active document에서 server → client event | SSE | one-way text event, replay cursor, HTTP streaming/hosting owner | client message, binary, arbitrary header/handshake가 핵심 |
| active document의 진짜 duplex interaction | WebSocket | presence/collaboration/interactive command, subprotocol·resume·queue owner | server notification만 필요하거나 HTTP command가 충분 |
| inactive/terminated document에 user-visible notification | Web Push | 명시적 permission UX, Service Worker, backend subscription/provider owner | live UI, ordered delta, 보장된 즉시성 또는 silent sync가 목적 |
| relaxed freshness 또는 stream fallback | bounded polling | snapshot/conditional endpoint, finite lease와 request budget | 짧은 주기로 push를 흉내 내거나 hidden 상태에서 계속 실행 |
| focus/reconnect 시 최신화면 충분 | 기존 Query refetch | freshness SLO를 만족 | 추가 transport를 설치할 이유가 없음 |
| 사용자가 직접 갱신해도 충분 | manual refresh | stale 상태를 명확히 표시 가능 | 자동 background work가 불필요 |
SSE를 지원하지 않는다는 이유만으로 WebSocket으로 자동 전환하지 않는다. WebSocket duplex protocol을 Polling으로 조용히 축소하지도 않는다. fallback은 같은 사용자 의미와 server authority를 보존할 때 registry에 명시한 한 경로만 선택한다.
Web Push는 foreground transport의 fallback이 아니다. active document가 없을 때 사용자에게 알리는 보완 채널이며, document가 열리면 snapshot/refetch 후 SSE 또는 WebSocket이 독립적으로 연결된다.
2.2 선택 절차
measured freshness/interaction requirement
-> focus/refetch/manual refresh로 충족 가능한지 확인
-> direction/background/delivery 의미 분류
-> server replay/snapshot/auth/retention owner 확인
-> 하나의 primary foreground transport 선택
-> 의미가 같은 bounded polling fallback 선택 여부
-> Web Push 보완 요구를 별도 선택
-> failure UX, resource budget와 kill switch 승인
-> deterministic/provider/browser/operations evidence
-> canary traffic admission
다음 중 하나라도 없으면 product selection은 NOT_SELECTED다.
- 측정 가능한 freshness 또는 interaction SLO
- event/snapshot protocol owner
- authorization와 account transition 계약
- replay retention 또는 명시적인 snapshot-only 복구
- connection/request/push 비용 owner
- stale/degraded/unsupported UX
- 실제 backend/provider test environment
2.3 인접 server-stream protocol과의 경계
API contract, Schema, Mapper와 Server State의 Connect/gRPC-Web server stream과 향후 GraphQL subscription은 이 문서의 SSE/WebSocket과 자동으로 같은 adapter가 아니다.
- Connect/gRPC-Web server stream은 registry-owned operation 하나의 request/response
lifecycle, protocol-specific envelope/trailer와 total deadline을 API platform이 소유한다.
application orchestrator가 mapped
AsyncIterable<Result<Event>>를 소비하는 operation-bound outbound result일 수 있으며, 항상 unsolicited inbound notification으로 재분류하지 않는다. - GraphQL subscription은 현재
NOT_SELECTED이며 persisted query adapter에 암묵적으로 추가하지 않는다. GraphQL@defer/@stream은 finite incremental HTTP response profile이지 subscription이나 realtime delivery가 아니다. - generated protobuf message를
REALTIME_EVENT_V1JSON envelope로 다시 감싸지 않는다. API adapter가 frame decode, semantic schema와 pure boundary mapping을 끝낸 뒤, 제품이 runtime-wide notification projection을 명시적으로 선택한 branch에서만 mapped event를 common scope/generation/dedupe/gap/resync coordinator 또는FeatureEventInput에 전달한다. - transport frame decode, media/subprotocol, status/trailer와 reconnect는 각 API adapter가 계속 소유한다. 특히 Connect/gRPC-Web은 첫 event 전 transport replay만 허용하고 이후 reconnect에는 등록된 resume protocol이 필요하다는 VD-29/VD-27 규칙을 우선한다.
- API operation registry의 현재
SERVER_STREAM은CONNECT_HTTP | GRPC_WEB + rpcKind=SERVER_STREAM만 허용한다. SSE, WebSocket과 Web Push는 realtime registry가 소유하고, bounded polling은 terminal·replay-safe RESTQUERYoperation에 적용하는 scheduling policy다. - bounded polling의 한 logical attempt는 REST operation registry의 credential, conditional request, schema/mapper와 deadline을 재사용한다. Poll 전용 operation은 transport retry를 끄며 이 문서는 완료된 attempt 사이 cadence와 failure backoff만 소유한다.
같은 stream을 Connect/gRPC-Web과 SSE로 자동 failover하지 않는다. wire와 resume 의미가 같다는 backend contract와 별도 rollout evidence가 있을 때만 명시된 fallback으로 선택한다.
3. Topology와 clean architecture 배치
3.1 Window runtime
bootstrap composition root
-> immutable RealtimePolicyRegistry
-> session/account scope authority
-> lifecycle/online clock adapters
-> selected connection owner
-> SSE adapter
-> or WebSocket adapter
-> or no live connection
-> event protocol decoder
-> stream coordinator
-> dedupe/order/gap/resume
-> feature event input
-> query invalidation bridge
-> authoritative resync gateway
-> optional bounded polling coordinator
-> redacted diagnostics
page나 feature hook이 native connection을 직접 생성하지 않는다. route는 registry-owned logical subscription lease를 획득하고 release할 뿐이다. WebSocket runtime은 같은 endpoint/protocol의 logical subscription을 multiplex할 수 있지만 서로 다른 account scope나 protocol version을 같은 physical connection에 섞지 않는다. SSE baseline은 §6의 단일 session feed를 local dispatch할 뿐 server-side logical subscription을 multiplex하지 않는다.
권장 application 의미 경계의 예시는 다음과 같다.
type ExternalEventEffect =
| Readonly<{ kind: "IGNORE" }>
| Readonly<{ kind: "INVALIDATE"; topic: QueryInvalidationTopic }>
| Readonly<{ kind: "RESYNC"; stream: StreamRegistrationId }>
| Readonly<{ kind: "APPLY_EPHEMERAL"; viewEvent: ViewEvent }>;
interface FeatureEventInput {
handleExternalEvent(
event: FeatureExternalEvent,
context: ExternalEventContext,
): Promise<Result<ExternalEventEffect>>;
}
이 예시는 target 의미를 설명하며 현재 production type이 아니다.
QueryInvalidationTopic, event union과 failure는 실제 feature registry가 소유한다.
application use case가 QueryClient를 호출하지 않고, presentation query bridge가
INVALIDATE를 canonical query key 동작으로 투영한다.
3.2 Service Worker runtime
Web Push는 window bootstrap과 다른 실행 환경이다.
service worker composition root
-> exact worker/release protocol
-> strict push envelope codec
-> bounded push event handler
-> notification policy registry
-> safe notification renderer
-> notificationclick route mapper
-> optional client-window handoff
Service Worker를 realtime 때문에 선택해도 offline shell, fetch interception, private response cache와 background sync가 자동으로 승인되지 않는다. 기존 Cache Storage reference runtime과 worker registration은 인접하지만 별도 capability다.
하나의 origin과 scope에는 worker registration/update owner가 하나여야 한다.
realtime용 worker를 별도 파일로 겹쳐 등록하지 않고, 선택된 worker capability가
하나의 build/composition root에서 push, notificationclick, install/activate
handler를 조립한다.
worker는 React presentation이 아니지만 외부 event가 application 의도를 구동한다는 inbound 원칙을 동일하게 따른다. worker 전용 codec과 use-case facade는 DOM이나 window runtime을 import하지 않는다.
3.3 Polling의 소유 위치
일반 server-state freshness polling은 별도 PollingPort가 아니라 query bridge의
scheduling policy다. 기존 query key, AbortSignal, focus/reconnect refetch와 같은
owner가 중복 실행을 막는다.
업무상 long-running job의 terminal 상태를 기다리는 polling은 application
orchestrator가 기존 feature gateway와 ClockPort를 사용한다. 이 경우 poll
lease의 max attempts, max elapsed, terminal states와 cancellation이 use case
계약에 포함된다.
어느 경우에도 page component의 setInterval이나 각 HTTP/Query/SDK의 독립 retry
loop로 구현하지 않는다.
4. Registry와 공통 event protocol
4.1 Immutable registration
선택된 stream은 closed registry row를 가져야 한다.
type RealtimeStreamRegistration = Readonly<{
id: StreamRegistrationId;
protocol: "REALTIME_EVENT_V1";
owner: FeatureId;
scope: "ORIGIN_SHARED" | "ACCOUNT_BOUND" | "SESSION_BOUND";
primaryTransport: "SSE" | "WEBSOCKET" | "NONE";
endpointId: RealtimeEndpointId;
eventTypeIds: readonly EventTypeId[];
delivery: "INVALIDATION_HINT" | "AUTHORITATIVE_DELTA" | "EPHEMERAL";
recovery: RealtimeRecoveryProfile;
fallback: "BOUNDED_POLLING" | "EXPLICITLY_STALE";
hiddenPolicy: "CLOSE" | "BOUNDED_GRACE";
limits: RealtimeLimits;
killSwitchId: KillSwitchId;
}>;
type RealtimeEventTypeRegistration = Readonly<{
id: EventTypeId;
owner: FeatureId;
payloadSchemaId: RuntimeSchemaId;
mapperId: BoundaryMapperId;
effectProfileId: ExternalEventEffectProfileId;
stateBearing: boolean;
}>;
type RealtimeRecoveryProfile =
| Readonly<{
mode: "CURSOR";
snapshotOperationId: ApiOperationId;
checkpointCodecId: RuntimeSchemaId;
barrier: "REPLAY";
}>
| Readonly<{
mode: "SNAPSHOT_ONLY";
snapshotOperationId: ApiOperationId;
checkpointCodecId: RuntimeSchemaId;
barrier: "CONNECT_BUFFER" | "SERVER_HOLD" | "NONE";
}>
| Readonly<{
mode: "SESSION_REBUILD";
rebuildInputId: ApplicationInputId;
}>;
실제 config schema는 exact key, duplicate ID, unknown reference, contradictory transport/fallback과 implementation ceiling 초과를 startup에서 거절한다. registry는 construction 시 copy/freeze하며 request마다 endpoint, topic, scope, payload limit, retry와 fallback을 override하지 못한다.
channel: string이나 arbitrary URL을 application caller가 제공하지 않는다.
V1 logical subscription input은 registry ID만 허용한다. server-side subset
filter는 contiguous stream-wide sequence를 깨뜨릴 수 있으므로 NOT_SELECTED다.
filter가 필요하면 별도 StreamRegistrationId, stream epoch, contiguous sequence와
checkpoint를 가진 stream으로 등록한다. common sequence/checkpoint 처리 뒤의
feature-local dispatch predicate는 server subscription/cursor 의미를 바꾸지
않는다.
state-bearing event가 하나라도 있는 stream은 CURSOR | SNAPSHOT_ONLY와 registered
snapshot/checkpoint를 가져야 한다. SESSION_REBUILD는 모든 event가 EPHEMERAL인
stream에만 허용한다. contradictory recovery/delivery/barrier는 startup에서
거절한다.
4.2 Target event envelope
공통 transport가 전달하는 target envelope의 의미는 다음과 같다.
type RealtimeEventBase<TPayload> = Readonly<{
protocol: "REALTIME_EVENT_V1";
streamId: StreamRegistrationId;
streamEpoch: string;
eventType: EventTypeId;
eventId: string;
sequence: string;
occurredAt: string;
scopeBinding: string;
payload: TPayload;
}>;
type RealtimeEventEnvelope<TPayload> = Readonly<
RealtimeEventBase<TPayload> &
(
| Readonly<{ recoveryMode: "CURSOR"; resumeCursor: string }>
| Readonly<{
recoveryMode: "SNAPSHOT_ONLY" | "SESSION_REBUILD";
resumeCursor: null;
}>
)
>;
protocol은 exact literal이고 unknown/newer version은 fail-closed한다.streamId와eventType은 registry에 존재해야 한다.streamEpoch은 server stream reset, partition rebuild 또는 호환 불가능한 replay change 때 바뀌는 opaque identifier다.eventId는 bounded dedupe용 opaque ID다.sequence는 stream + epoch 안에서만 단조 증가하는 canonical unsigned decimal string이다. JSONnumber의 safe-integer 한계를 피하고 parse 뒤 bounded integer representation으로 비교한다.CURSOR의 non-emptyresumeCursor만 server-issued opaque replay position이다. event ID나 sequence와 같은 문자열일 수 있어도 의미는 별도다.SNAPSHOT_ONLY | SESSION_REBUILD는 cursor를 발급·전송하지 않고 exactresumeCursor: null을 사용한다. 빈 문자열이나 synthetic cursor를 만들지 않는다.occurredAt은 strict RFC 3339 timestamp지만 ordering authority가 아니다.scopeBinding은 raw account/session ID나 cache-key fingerprint가 아닌 session/BFF-issued opaque exact-match token이다. authenticated session snapshot과 server subscription에 같은 값을 bind하고 scope switch마다 rotate한다. client nonce profile을 선택하면 authenticated fetch/WebSocket subscribe에서 server가 exact echo해야 하며 native EventSource에는 사용하지 않는다.payload는 event type별 closed codec와 더 작은 byte/count/depth limit를 가진다.
credential, access token, email, account/tenant/user ID, signed URL, PushSubscription endpoint/key와 notification 자유 문구는 envelope에서 금지한다. 제품에 식별자가 필요하면 backend가 authorization한 opaque resource reference를 사용한다.
scopeBinding은 authorization proof가 아니다. server는 connect/resume와 각
logical subscription을 current credential로 다시 인가한다. CURSOR는
protocol + stream/feed + streamEpoch + registered subscription set + authorization scope에 server-side로 bind한다. client는 cursor를 해석하거나
수정하지 않고 header-safe character/byte ceiling만 검사해 전달한다. 다른
stream/account/subscription의 cursor 재사용이나 tamper는 RESET_REQUIRED 또는
FORBIDDEN으로 닫는다.
기존 recipe의 RealtimeEvent<T>는 이 target envelope로 구현됐다는 증거가
아니다. sequence: number와 generic payload를 가진 copyable example일 뿐이며,
선택 branch에서 breaking recipe amendment 또는 feature-local contract로
교체한다.
4.3 Decode와 적용 순서
모든 foreground transport는 다음 순서를 지킨다.
raw bytes/frame
-> transport byte ceiling
-> UTF-8 / exact media or frame type
-> JSON syntax + object depth/count ceiling
-> protocol/version/exact-key schema
-> registry stream/event lookup
-> current scope + generation fence
-> stream epoch + sequence + dedupe check
-> ValidatedRealtimeEventDto (adapter-private)
-> registered pure boundary mapper
-> FeatureExternalEvent
-> sequential feature effect
-> local effect commit
-> last-applied sequence + conditional CURSOR advance
-> optional selected WebSocket/event-protocol ACK
CURSOR를 effect보다 먼저 저장하면 crash 시 event를 잃을 수 있다. effect 뒤에 저장하면 crash window에서 duplicate가 다시 올 수 있으므로 effect가 idempotent 해야 한다. non-CURSOR profile도 sequence/checkpoint를 effect 뒤에 갱신한다. memory-only invalidation hint는 duplicate invalidate를 허용한다. authoritative delta나 durable command effect는 별도 idempotency ledger 없이는 선택하지 않는다.
schema/mapper가 실패하면 effect, cursor/ACK와 cache write는 모두 0이다.
MAPPING_CONTRACT_VIOLATION으로 닫고 state-bearing event는 snapshot resync한다.
ACK가 있는 protocol에서도 ACK는 idempotent하고 bounded해야 하며 business
commit, 사용자 확인 또는 read proof가 아니다. SSE에는 application ACK 경로가
없다.
callback 하나의 실패가 connection event loop를 깨거나 다음 event와 병렬로
뒤섞이지 않게 stream별 sequential queue를 사용한다. V1 sequence는 stream-wide
하나뿐이다. partition ordering이 필요하면 각 partition을 별도 logical stream으로
등록한다. 한 envelope 안의 partition별 ordering/concurrency는 V2에서 closed
partitionId와 partition별 checkpoint/ceiling을 추가하기 전에는 허용하지 않는다.
4.4 Duplicate, out-of-order와 gap
stream + epoch별 lastAppliedSequence를 기준으로 처리한다.
| 입력 | 동작 |
|---|---|
이미 처리한 eventId |
payload를 다시 적용하지 않고 duplicate 관측만 남김 |
같은 eventId인데 sequence/payload가 다름 |
protocol conflict; cursor 미진행, close + resync |
같은 sequence인데 다른 eventId |
protocol conflict; cursor 미진행, close + resync |
sequence <= lastApplied |
stale/out-of-order로 drop |
sequence == lastApplied + 1 |
순차 적용 |
sequence > lastApplied + 1 |
gap; delta 적용 중지, RESYNC_REQUIRED |
unknown streamEpoch |
old/new epoch를 섞지 않고 registered snapshot 또는 session rebuild |
| dedupe/queue ceiling 초과 | 임의 eviction 후 계속 적용하지 않고 registered recovery |
| captured old runtime generation | late callback safe drop + aggregate diagnostic |
current connection의 scopeBinding mismatch |
effect/sequence/cursor 0, freshness UNKNOWN, close + session revalidation + registered recovery |
작은 out-of-order reorder buffer를 선택할 수 있지만 count, bytes와 wait deadline을 registry가 제한해야 한다. 기본은 reorder하지 않고 gap으로 처리하는 것이다.
gap 뒤에는 뒤 이벤트를 “최신처럼 보이므로” 적용하지 않는다. 다음 흐름은 state-bearing stream에 적용한다.
gap detected
-> pause stream apply
-> mark freshness UNKNOWN/RESYNCING
-> authoritative snapshot request
-> validate snapshot + checkpoint
-> current generation의 required projection 적용 완료
-> reset dedupe/order state
-> checkpoint nextExpectedSequence부터 resume
snapshot request가 실패하면 stale UI와 manual retry를 표시한다. reconnect만 반복해 gap을 복구했다고 주장하지 않는다.
type SnapshotCheckpoint = Readonly<{
streamEpoch: string;
lastAppliedSequence: string;
snapshotRevision: string;
} & (
| Readonly<{ recoveryMode: "CURSOR"; resumeCursor: string }>
| Readonly<{ recoveryMode: "SNAPSHOT_ONLY"; resumeCursor: null }>
)>;
snapshot과 checkpoint는 같은 server commit point의 의미로 발급한다. subscribe
ack도 recovery mode와 일치하는 acceptedCursor: string | null 및
nextExpectedSequence를 반환한다. frontend는 TanStack 여러 projection 사이의
transaction을 과장하지 않고, current generation에서 필요한 projection 적용이
모두 성공하기 전에는 cursor/sequence state를 바꾸거나 stream effect를 재개하지
않는다.
EPHEMERAL + SESSION_REBUILD gap/epoch/overflow는 snapshot을 가장하지 않는다.
connection을 닫고 presence/collaboration session을 registered rebuild input으로
다시 연다. rebuild가 실패하면 interaction을 disabled/degraded로 표시한다.
4.5 Backend commit, outbox와 replay authority
frontend 정확성은 backend event publication 계약에 의존한다.
- 업무 transaction commit 전에 “완료” event를 publish하지 않는다.
- database state와 event ledger의 이중 쓰기는 transactional outbox, 동일 commit log 또는 동등한 server-owned mechanism으로 해결한다.
- broker offset이나 Kafka partition을 frontend wire에 직접 노출하지 않는다.
- server가 stream/subject authorization을 매 connect/subscribe/resume마다 다시 확인한다.
- replay retention과 cursor expiry 시간을 문서화한다.
- 오래된 cursor는 빈 성공 stream이 아니라 explicit
RESET_REQUIRED의미를 반환한다. - snapshot 응답은 함께 사용할
SnapshotCheckpoint를 같은 commit point의 의미로 발급한다. - server scale-out instance가 바뀌어도 같은 logical stream의 ordering 의미가 보존돼야 한다.
backend가 replay를 제공하지 않으면 state-bearing registration은
SNAPSHOT_ONLY다. snapshot과
connect 사이 event를 보존하는 server barrier가 없다면 “snapshot 먼저, 그 뒤
connect”만으로 CURRENT를 보장하지 못한다. CURRENT가 필요하면
connect-and-bounded-buffer → snapshot/checkpoint → checkpoint 이후 event 적용,
또는 server가 checkpoint 이후 event를 hold하는 동등한 barrier를 제공해야 한다.
그 barrier가 없는 profile은 reconnect/restore마다 finite revalidation을
수행하되 freshness를 STALE/UNKNOWN으로 표시하고 manual refresh를 제공한다.
5. 공통 lifecycle
5.1 서로 다른 상태 축
상태를 connected: boolean 하나로 표현하지 않는다.
| 축 | canonical 상태 |
|---|---|
| connection | IDLE, CONNECTING, OPEN, BACKING_OFF, PAUSED, DRAINING, CLOSED |
| freshness | UNKNOWN, CURRENT, STALE, RESYNCING |
| authorization | UNKNOWN, VALID, REFRESHING, REQUIRED, FORBIDDEN |
| availability | UNKNOWN, AVAILABLE, DEGRADED, UNSUPPORTED, UNAVAILABLE |
| admission | DISABLED, SHADOW, CANARY, ENABLED |
UI에는 필요한 projection만 노출한다.
LIVE
RECONNECTING
STALE
AUTH_REQUIRED
UNSUPPORTED
OPEN + UNKNOWN, PAUSED + CURRENT, BACKING_OFF + STALE처럼 조합될 수 있다.
socket open만으로 “최신” badge를 표시하지 않는다.
5.2 Runtime 생성과 lease
runtime config/release verified
-> registry validated
-> traffic admission checked
-> 새 factory가 DISABLED: network side effect 없이 종료
-> local static capability probe
-> session recovery settled
-> immutable scope/generation acquired
-> selected adapter and coordinator created
-> SHADOW: 승인된 synthetic hosting probe만 허용
-> CANARY/ENABLED: route/session subscription lease 허용
route-scoped subscription은 마지막 consumer가 사라지면 logical unsubscribe한다. session-wide notification badge처럼 명시적으로 등록된 subscription만 route unmount 뒤 남을 수 있다.
React StrictMode mount → cleanup → mount에서도 physical listener, timer와
subscription이 중복되지 않아야 한다. lease release와 runtime close()는
terminal/idempotent다.
TrafficAdmission=DISABLED는 새 data-plane runtime/lease/send/subscribe/poll을
막는다. 이미 소유한 resource를 안전하게 없애는 fixed, idempotent
close/unsubscribe/revoke는 connection DRAINING의 bounded cleanup plane에서만
허용한다. cleanup caller는 captured resource/association ID만 사용할 수 있고 새
endpoint/topic을 만들거나 retry loop를 시작하지 않는다. CLOSED 뒤에는 cleanup을
포함한 network side effect가 0이다.
5.3 Reconnect와 retry owner
reconnect는 full-jitter exponential backoff를 사용한다.
localDelay = random(0, min(maxDelay, baseDelay * 2^attempt))
effectiveDelay = max(localDelay, validServerNotBefore)
- base/max delay, max attempts와 max elapsed를 registry ceiling 안에서 고정한다.
- HTTP
Retry-After, SSEretry와 protocol retry hint는 서로 다른 wire 의미지만 모두 server가 요구한 시각보다 먼저 재시도하지 않는 not-before bound로 취급한다.effectiveDelay가 implementation max delay나 남은 elapsed budget을 넘으면 낮춰 clamp하지 않고 reconnect/poll을 종료해DEGRADED/STALE로 전환한다. - connection이 잠깐 열렸다는 이유로 attempt를 0으로 만들지 않는다. stable-open window 또는 valid heartbeat/event 뒤에만 reset한다.
- stream reconnect는 realtime recovery coordinator, credential recovery는 session owner, Poll attempt 사이 cadence는 poll coordinator가 각각 소유한다. Poll-bound REST transport retry와 Query retry는 끈다. 같은 logical recovery의 effect writer나 timer가 둘 이상 활성화되지 않게 generation으로 배타화한다.
offline상태에서는 timer로 재시도하지 않고 online hint를 기다린다.navigator.onLine은 실제 backend reachability authority가 아니다.- auth expiry는 session owner의 single-flight recovery를 한 번 요청한다.
실패하면
AUTH_REQUIRED로 멈춘다. - protocol/schema/forbidden/policy failure는 retry하지 않는다.
- 외부 close/result의 rate limit/provider unavailable은 유효한 bounded server not-before hint가 있을 때만 재시도한다. hint가 없거나 ceiling 밖이면 terminal이다. adapter 내부 provider throw처럼 server 응답이 아닌 실패만 남은 retry budget 안에서 local full-jitter를 사용할 수 있다.
- budget을 소진하면 infinite reconnect가 아니라
DEGRADED와 명시된 fallback 또는 stale UI로 전환한다.
authoritative recovery와 새 transport 사이에는 값 동등성이 아닌 한 generation의 exact proof barrier를 둔다.
stream recovery commits exact branded RECOVERY_COMMITTED checkpoint
-> reconnect owner retains that exact object
-> next attempt receives initialRecovery=RECOVERY_RECONNECT
-> SSE onOpen / WebSocket onSubscribed proves the same checkpoint
-> attempt returns the same recovery proof
-> common coordinator confirms the transport barrier
-> readiness gate settles
-> only then may the new transport admit events
checkpoint clone, 누락된 proof, 다른 stream/generation의 proof와 readiness deadline 초과는 fail-closed다. gate의 성공·실패·abort 모든 경로는 반드시 settle하며, attempt당 상한은 30초다.
5.4 Visibility, page lifecycle와 network
| signal | 기본 동작 |
|---|---|
visibilitychange -> hidden |
새 Polling 중지, configured grace 뒤 live connection pause/close |
visibilitychange -> visible |
scope 확인, snapshot freshness gate, 그 뒤 resume/connect |
offline |
request/connection attempt abort, backoff timer 정지 |
online |
즉시 flood하지 않고 jitter 후 authoritative reachability check |
pagehide |
document-owned SSE/WS/Poll 모두 close; unload-only write 금지 |
pageshow/bfcache restore |
old callback generation 폐기, snapshot/resume gate |
| discarded/reloaded document | memory cursor를 신뢰하지 않고 snapshot 또는 scope-bound resume |
| Service Worker termination | in-memory queue/cursor가 보존된다고 가정하지 않음 |
hidden document의 timer는 throttling되므로 heartbeat deadline만으로 즉시 connection failure를 선언하지 않는다. hidden grace를 선택한 경우에도 implementation hard ceiling 뒤에는 close한다.
beforeunload/unload에서 unsubscribe, cursor write, push revoke나 server logout이
완료된다고 가정하지 않는다. local session notification과 server-side TTL/revoke가
authority다.
pagehide/freeze 뒤 session-wide logical subscription metadata가 남더라도 physical
connection은 document owner와 함께 닫는다. 다음 document/runtime은 current
session과 checkpoint를 다시 검증한 뒤 새 connection을 만든다.
5.5 Logout, account/tenant switch와 release
session authority announces local transition
-> admission DISABLED
-> connection DRAINING
-> old generation FENCED
-> reject new lease/send/poll
-> abort connect/read/backoff/snapshot
-> close SSE/WebSocket
-> clear memory cursor/dedupe/queue
-> revoke old push account association through bounded request
-> dispose listeners/timers
-> CLOSED
-> construct new scope independently
late socket event, poll response, auth recovery, snapshot, notification handoff와 Service Worker message는 old generation/scope면 적용하지 않는다.
long-lived connection은 connect-time authorization만 신뢰하지 않는다. backend는
active subscription의 permission/session revoke를 control event 또는 close로
전파하고, registry-owned maxConnectionAge/reauth deadline 안에 current
credential로 재연결하게 한다. SSE처럼 별도 subscribe command가 없는 transport는
권한 변경 시 server가 stream을 종료해야 한다. server hint는 client hard
connection-age ceiling을 늘릴 수 없다.
Web Push browser subscription 자체와 account association은 구분한다. logout의
정상 security commit은 local REVOKED fence와 backend old-account association
revoke 둘 다다. 둘 중 하나가 ambiguous하면 완전한 push revoke를 주장하지 않고
§8.3의 unavailable/reconciliation 경로로 내린다. browser unsubscribe()는 제품
정책에 따른 best-effort cleanup이며, 그 실패 때문에 old account payload가
노출되지 않도록 push envelope와 notification copy가 scope-safe해야 한다.
release가 event codec, worker protocol이나 registry compatibility를 깨면 release epoch를 바꾸고 old stream/worker message를 거절한다. worker update 중 old/new page가 공존할 수 있으므로 지원하는 N/N-1 wire window 또는 fail-closed reload policy를 명시한다.
5.6 여러 tab
server stream과 BroadcastChannel을 하나의 bus로 합치지 않는다. server stream은
backend authorization/replay authority이고 cross-tab channel은 같은 origin 안의
best-effort hint다.
기본 reference 설계는 tab별 runtime이되 다음을 제한한다.
- tab 하나당 selected primary transport physical connection 1개
- WebSocket logical subscription multiplexing 또는 SSE single-feed local dispatch
- hidden tab close/pause 정책
- visible 복귀 시 authoritative snapshot
SharedWorker나 leader election으로 origin 전체 connection을 하나로 만드는 기능은 기본 목표가 아니다. leader crash, storage partition, worker 지원과 exactly-once handoff를 별도로 설계해야 한다. 여러 visible window의 connection 수가 provider budget을 넘는 제품은 별도 multi-context coordinator를 선택한다.
다른 tab이 수신한 event를 BroadcastChannel로 전달해도 그것은 invalidation optimization일 뿐이다. 수신하지 못한 tab은 focus/snapshot으로 수렴해야 한다.
6. SSE
6.1 선택 의미
SSE는 active document에서 server-to-client text event만 필요할 때 우선한다. 일반 업무 command는 기존 HTTP gateway를 사용한다.
장점:
- HTTP semantics와 proxy/observability 인프라를 재사용
- event ID와 reconnect model이 표준에 있음
- one-way 요구를 duplex protocol로 과장하지 않음
제약:
- native
EventSourceconstructor는 URL과withCredentials만 받고 arbitrary request header를 받지 않는다. - native reconnect의 세부 status/header와 bounded retry를 application이 충분히 제어하기 어렵다.
- active document가 없거나 frozen/terminated되면 background delivery를 보장하지 않는다.
- HTTP/1.x, proxy buffering/idle timeout과 tab 수에 영향을 받는다.
6.2 Baseline adapter 선택
공통 reference target은 same-origin BFF의 fetch-stream SSE adapter다.
fixed same-origin HTTPS endpoint
-> credential attacher / HttpOnly session cookie
-> fetch with AbortSignal
-> exact status/content type
-> bounded UTF-8 SSE parser
-> common event envelope decoder
이 선택은 다음을 제어하기 위함이다.
- fixed headers와 opaque session integration
200/204/401/403/409/410/429/503의 closed mapping- redirect 금지
- read/idle deadline와 total event buffer
- bounded reconnect owner
- lifecycle close와 current cursor 명시
native EventSource는 다음 조건을 모두 만족할 때 별도 adapter profile로 허용할
수 있다.
- same-origin HttpOnly cookie 인증
INVALIDATION_HINT + SNAPSHOT_ONLYrecovery profile- status별 상세 UX가 control event와
204만으로 닫힘 - URL에 credential/cursor secret이 없음
- adapter close/recreate 뒤 snapshot/checkpoint recovery가 있음
- target browser/hosting evidence가 있음
native EventSource는 UA가 관리하는 last-event-ID를 application effect commit과
묶을 수 없다. 따라서 이 profile은 INVALIDATION_HINT 전용이며
AUTHORITATIVE_DELTA에는 금지한다. error/자동 reconnect/visibility·page restore
때마다 generation을 fence하고 authoritative snapshot freshness gate를 수행한다.
gate 동안 도착한 validated hint는 effect를 바로 쓰지 않고 bounded one-bit
pendingInvalidation으로 coalesce하며, checkpoint 뒤 pending refetch까지 끝내기
전에는 CURRENT를 표시하지 않는다. 이 post-checkpoint drain 또는 동등한 server
hold/replay barrier를 제공하지 못하면 profile은 계속 STALE/UNKNOWN이다.
cursor-after-effect 또는 exact delta replay가 필요한 stream은 fetch-stream
profile만 허용한다.
SSE baseline은 registry-owned SESSION_FEED 하나를 사용한다. CURSOR profile만
feed-wide replay cursor 하나를 가지며 SNAPSHOT_ONLY는 cursor를 전송하지
않는다. route lease는 수신 event의 local dispatch만 제어하며 server
subscription set을 동적으로 바꾸지 않는다. SSE에는 client→server
SUBSCRIBE/UNSUBSCRIBE frame이 없고 Last-Event-ID도 physical request당
하나이므로, arbitrary multi-stream multiplex와 per-subscription cursor는
NOT_SELECTED다. 필요하면 stream별 physical SSE와 더 작은 connection ceiling,
또는 별도 typed HTTP control plane/composite cursor protocol을 새 ADR로 선택한다.
6.3 HTTP와 parser 계약
fetch-stream baseline:
method = GET
credentials = same-origin
redirect = error
cache = no-store
referrerPolicy = no-referrer
Accept = text/event-stream
Last-Event-ID = approved current cursor, CURSOR profile only
- endpoint는 composition-owned fixed absolute HTTPS URL이다.
- cross-origin은 explicit origin, exact CORS, CSP
connect-src, credential와 preflight 계약을 별도 승인해야 한다. - token, account ID나 unrestricted topic을 URL/query에 넣지 않는다.
- 성공은 exact
200과 normalizedtext/event-stream만 허용한다. - stream은 UTF-8로만 decode한다.
- BOM, CR/LF/CRLF, comments, multi-line
data,event,id,retry를 표준 semantics로 처리한다. - NUL/CR/LF가 포함된 invalid event ID를 cursor로 저장하지 않는다.
- fetch parser는 parsed candidate ID와 application-committed reconnect cursor를
분리한다.
CURSOR의 각 application event block은 non-emptyid를 직접 포함하고 decoded envelope의resumeCursor와 exact match해야 한다. 이전 block의 inherited SSE ID만으로 통과시키지 않는다.SNAPSHOT_ONLY | SESSION_REBUILD는id를 발급하지 않고 envelope cursor도 exactnull이어야 한다. - event blank line 전 EOF는 incomplete event로 폐기한다.
- comments/heartbeat도 line과 idle budget을 소비하지만 application event를 만들지 않는다.
retry값은 숫자 syntax와 registry ceiling을 통과한 경우 다음 delay hint로만 사용한다.- compressed/decompressed bytes 중 더 큰 보수적 측정을 ceiling에 적용한다.
상태 mapping:
| 결과 | 동작 |
|---|---|
200 text/event-stream |
parser 시작 |
204 |
server terminal close; 자동 reconnect 금지 |
401 |
session recovery 한 번, 실패 시 AUTH_REQUIRED |
403 |
FORBIDDEN, retry 금지 |
409/410 with exact reset contract |
cursor expired, snapshot resync |
429 |
bounded Retry-After, retry budget 소비 |
502/503/504 |
bounded provider backoff |
| redirect, wrong type, malformed stream | protocol/policy failure, fail-closed |
generic HTTP client의 전체-body decoder와 retry를 우회하되 credential, diagnostics, fixed endpoint와 failure vocabulary는 같은 platform policy를 재사용한다.
6.4 Heartbeat, resume와 hosting
server는 application event가 없어도 policy 범위의 comment heartbeat를 보낸다. client watchdog은 last received byte/comment/event 시간을 관측한다. hidden/frozen 상태에서는 watchdog을 pause하거나 connection을 닫아 false timeout을 만들지 않는다.
CURSOR에서만 SSE event id를 transport resume cursor로 사용하고 JSON
envelope의 resumeCursor와 exact equality를 검사한다. 다르면 protocol
failure다. application event ID와 cursor가 우연히 같아도 두 의미를 합치지
않는다. 이 cursor는 위의 feed/subscription-set, scope와 epoch binding을
만족해야 하며 authorization proof로 사용하지 않는다.
hosting/provider evidence는 다음을 포함한다.
- proxy/CDN response buffering 비활성 또는 streaming flush 증거
Cache-Control: no-store와 transform/cache 금지- idle/request duration과 heartbeat 호환
- HTTP/2 또는 승인 connection budget
- deploy/drain 중 reconnect storm 제한
- load balancer와 backend의 replay cursor 일관성
- client disconnect 후 server resource cleanup
local parser test만으로 hosting readiness를 주장하지 않는다.
7. WebSocket
7.1 선택 의미
WebSocket은 client와 server가 낮은 지연으로 지속적인 application message를 교환해야 할 때만 선택한다.
적합한 예:
- presence와 ephemeral collaboration signal
- server가 조정하는 interactive session
- HTTP 요청/응답으로 표현하기 어려운 duplex protocol
부적합한 예:
- 단순 server invalidation notification
- durable mutation을 idempotency ledger 없이 socket send로 바꾸는 것
- background notification
- 높은 event rate의 근거가 없는데 “실시간 같아 보이기” 위한 선택
7.2 Handshake와 인증
browser WebSocket constructor는 URL과 subprotocol만 제공한다. arbitrary
Authorization header를 기대하지 않는다.
baseline:
- fixed same-origin
wss:endpoint - Secure/HttpOnly/SameSite session cookie 또는 same-origin BFF가 소유한 짧은 handshake mechanism
- exact
Origin검증 - fixed versioned subprotocol 예:
realtime.v1 - CSP
connect-src - redirect 없음
access token, session token과 push capability를 URL/query 또는
Sec-WebSocket-Protocol에 넣지 않는다. subprotocol은 protocol negotiation
전용이다. cross-origin cookie를 선택하면 exact origin allowlist, CSWSH 방어,
SameSite 정책과 handshake authorization을 별도 threat model로 승인한다.
server는 handshake 성공만으로 이후 모든 logical subscription과 command를 허용하지 않는다. subscribe/resume/command마다 current subject, resource와 scope를 재인가한다.
7.3 Closed message protocol
기본 text JSON protocol은 다음 control message만 허용한다.
server -> WELCOME(protocol, connectionId, heartbeat, limits)
client -> SUBSCRIBE(subscriptionId, streamId, cursor|null, scopeBinding)
server -> SUBSCRIBED(
subscriptionId,
streamEpoch,
acceptedCursor|null,
nextExpectedSequence
)
server -> EVENT(subscriptionId, envelope)
server -> RESET_REQUIRED(subscriptionId, reason)
client -> UNSUBSCRIBE(subscriptionId)
server -> UNSUBSCRIBED(subscriptionId)
client -> HEARTBEAT(nonce)
server -> HEARTBEAT_ACK(nonce)
client/server -> CLOSE(category)
- exact top-level type/version와 key set을 검증한다.
connectionId, subscription ID와 nonce는 diagnostics에 raw로 남기지 않는다.- accepted cursor는 recovery mode/stream/scope binding을 다시 검증하고
nextExpectedSequence로 local checkpoint를 초기화한다. - same-epoch
CURSORresume이면nextExpectedSequence는 반드시 locallastAppliedSequence + 1이다. mismatch는 effect/cursor 0,RESET_REQUIRED와 snapshot recovery로 닫는다. server가 opaque accepted cursor를 rotate할 수는 있지만 requested cursor와 같은 logical position임을 conformance로 증명해야 하며 silent advance는 금지한다. - state-bearing initial/no-cursor subscribe는 snapshot/checkpoint와 selected
buffer/hold barrier를 통과하기 전
CURRENT나 event effect를 허용하지 않는다. UNSUBSCRIBE를 보낸 subscription은UNSUBSCRIBINGtombstone으로 남긴다. matchingUNSUBSCRIBEDACK 전에는 ID를 재사용하거나 quota를 반환하지 않고, 그 사이 늦게 온EVENT/RESET_REQUIRED/SUBSCRIBED는 effect 없이 버린다. unknown 또는 다른 state의 ACK는 protocol failure다. ACK가 bounded apply deadline 안에 오지 않으면 connection을 닫아 reconnect owner로 넘긴다.- server-advertised limit은 client implementation ceiling을 높일 수 없다.
- binary frame은 baseline에서 거절한다. 제품이 binary protocol을 선택하면 별도 codec/version, decompressed byte cap과 browser evidence가 필요하다.
- per-message compression은 decompression bomb와 memory ceiling을 검증한 provider profile에서만 허용한다.
업무 command를 추가하면 별도 closed operation registry, command ID,
idempotency, expected revision, authorization, ack/commit certainty와 retry
규칙을 정의한다. send() 반환 또는 bufferedAmount 감소는 server acceptance나
business commit이 아니다.
7.4 Heartbeat와 close
browser API는 protocol ping/pong을 application에 노출하지 않으므로 backend transport ping만으로 application freshness를 판정하지 않는다. 필요하면 위의 bounded application heartbeat/ack를 사용한다.
- server
WELCOME이 허용된 heartbeat range를 고정한다. - 한 번에 하나의 outstanding nonce만 둔다.
- ack deadline은 visibility와 network state를 고려한다.
- timeout은 connection을 close하고 common reconnect owner로 넘긴다.
- heartbeat timer를 route component마다 만들지 않는다.
close code/reason은 raw 문자열을 application이나 telemetry에 노출하지 않고 closed category로 mapping한다.
| category | 예시 의미 | reconnect |
|---|---|---|
NORMAL |
intentional close/drain | 아니오 |
RESTART |
server deploy/restart | bounded |
OVERLOADED |
server capacity/rate | exact server hint가 있을 때만 bounded; 없으면 terminal |
AUTH_REQUIRED |
session expired | single-flight recovery 뒤 한 번 |
FORBIDDEN |
policy/authorization | 아니오 |
PROTOCOL_MISMATCH |
version/schema/subprotocol | 아니오 |
CURSOR_RESET |
replay 불가 | snapshot 뒤 새 connection |
NETWORK_LOST |
abnormal/network | bounded |
server private reason, stack, resource ID나 credential text를 UI/log에 복사하지 않는다.
7.5 Backpressure
classic browser WebSocket은 incoming stream backpressure를 제공하지 않는다. 따라서 다음 상한이 필수다.
- raw frame/decompressed frame bytes
- sequential inbound queue count와 bytes
- per-subscription event rate
- parsing/apply deadline
- outbound
bufferedAmount - outbound queued message count/bytes
inbound queue overflow에서 오래된 delta를 임의로 버리고 계속하지 않는다.
classic browser WebSocket baseline은 receive pause가 없으므로 connection을
close하고 freshness를 UNKNOWN으로 바꾼 뒤 snapshot resync한다. server가
bounded PAUSE/PAUSED/RESUME control ACK와 발신 중단을 별도 protocol로
증명한 profile에서만 subscription pause를 허용한다.
outbound ceiling을 넘으면 새 ephemeral message를 typed DROPPED_BACKPRESSURE로
거절하거나 connection을 drain한다. durable command를 memory queue에 무제한
쌓지 않는다. offline command queue는 별도 NOT_SELECTED capability다.
reference runtime의 SUBSCRIBE/UNSUBSCRIBE/heartbeat/CLOSE는 하나의 FIFO
outbound queue를 통과하며, negotiated message count/queued bytes와 native
bufferedAmount를 함께 검사한다. 하나라도 넘으면 QUEUE_OVERFLOW,
retryable=false, OVERLOADED로 connection generation 전체를 닫고 snapshot
recovery를 요청한다.
8. Web Push와 persistent notification
8.1 다른 transport와의 관계
Web Push의 목적은 active document가 없을 수 있는 상태에서 Service Worker를 통해 작은 notification hint를 받는 것이다.
application server
-> authorized subscription registry
-> Web Push provider / push service
-> browser PushSubscription
-> Service Worker push event
-> strict hint validation
-> persistent notification
-> notificationclick
-> safe route + authoritative foreground refresh
Web Push는 다음을 보장하지 않는다.
- 즉시 delivery
- ordered delivery
- exactly-once
- 사용자 확인/read receipt
- silent background synchronization
- SSE/WebSocket과 같은 live connection
- push payload만으로 최신 server state
push가 늦거나 유실돼도 app을 열었을 때 HTTP snapshot/focus refetch가 올바른 상태로 수렴해야 한다.
W3C Push API의 declarative push message는 Service Worker handler가 실행되지
않아도 user agent가 notification을 표시하거나 handler 실패 시 fallback 표시를
할 수 있다. V1에서는 이 경로를 NOT_SELECTED로 둔다. outbound payload의
top-level web_push: 8030 declarative shape를 금지하고 오직 encrypted
WEB_PUSH_HINT_V1만 보낸다. declarative profile은 local association fence,
closed copy/click route와 logout cleanup을 동등하게 보장하는 별도 ADR 전에는
선택하지 않는다.
8.2 Permission UX
- secure context와 browser support를 먼저 확인한다.
- page boot, route enter, sign-in 직후 자동 prompt를 금지한다.
- 사용자가 notification 가치와 빈도를 이해한 뒤 명시적 action으로 요청한다.
default,granted,denied,unsupported,dismissed를 다른 결과로 표현한다.- denied를 반복 prompt로 우회하지 않는다.
- 기능 핵심 경로는 push permission 없이도 foreground inbox/status로 접근할 수 있어야 한다.
- consent copy, notification category, quiet hours와 해제 경로를 제품 owner가 승인한다.
notification permission은 application authorization이 아니다. push를 켰다는 이유로 account/resource 접근 권한을 부여하지 않는다.
8.3 Subscription lifecycle
user opt-in
-> active service worker registration verify
-> permission request
-> VAPID public applicationServerKey verify
-> PushManager.subscribe(userVisibleOnly = true)
-> capture endpoint + p256dh + auth inside adapter
-> authenticated fixed BFF registration
-> server returns opaque registration ID + associationEpoch + sessionBindingEpoch
-> durable local ACTIVE fence transaction completes
-> native material leaves application memory
- VAPID private key는 server/provider에만 존재한다. public key만 runtime config에 둘 수 있다.
- endpoint,
p256dh,auth는 message delivery capability material이다. application state, URL, local/session storage, BroadcastChannel, diagnostics와 analytics에 넣지 않는다. - backend는 endpoint의 raw value 대신 제한된 운영 영역에서 encrypted storage와 keyed fingerprint를 사용한다.
- subscription은 account association, device/browser installation, worker scope, permission revision과 VAPID key generation을 server record에 묶는다.
- backend register/revoke는 VD-23 registry의 fixed
COMMANDoperation이다. cookie session이면 exact CSRF profile이 필수다. register는 keyed idempotency 또는 server atomic installation upsert + terminal receipt를 제공하고, revoke는 duplicate/ALREADY_GONE을 성공으로 닫는 idempotent 의미를 가진다.associationEpoch은 server commit 뒤에만 반환한다. - browser 또는 push service가 subscription을 rotate/deactivate할 수 있으므로
pushsubscriptionchange하나에 의존하지 않는다. boot/foreground의PushManager.getSubscription(), authenticated server reconciliation, expiration, registration response와 provider404/410cleanup을 함께 처리한다. pushsubscriptionchangeworker handoff 자체도 lifecycle AbortSignal과 10초 deadline 안에서만 controlled window에 reconciliation hint를 보낸다.matchAll()이 signal을 따르지 않거나waitUntil()이 동기 예외를 내도 늦은postMessage를 허용하지 않고 degraded observation으로 닫는다.- re-subscribe가 실패하면 notification을 끄고 foreground fallback을 유지한다.
associationEpoch은 backend registration authority가 발급한다.
releaseEpoch은 signed/verified release manifest에서 온다. backend registration
뒤 local ACTIVE commit 전에 crash할 수 있으므로 boot에서 native subscription,
local fence와 server association을 모두 대조하기 전에는 push를 ACTIVE로
표시하지 않는다. orphan server record는 revoke하고, native/local만 남은 상태는
재인가·재등록하거나 fail-closed unsubscribe한다.
logout/account switch:
- durable
fenceGeneration을 먼저 rotate하고 association을REVOKED로 같은 transaction에 commit한다. - window의 old runtime generation을 fence한다.
- authenticated backend association revoke를 bounded request로 실행한다.
- 제품 정책에 따라 native
unsubscribe()를 best effort로 실행한다. - old association tag로 표시한 notification을 bounded
getNotifications()로 찾아 best-effort close한다. - 새 account opt-in은 기존 consent와 association 정책을 다시 평가한다.
backend revoke가 ambiguous하면 server subscription은 짧은 association TTL,
session/account epoch 확인과 send-time authorization으로 추가 방어한다.
local REVOKED transaction이 실패하면 “late push를 반드시 drop했다”고 주장하지
않는다. push result를 PUSH_UNAVAILABLE(reason=LOCAL_FENCE_UNSAFE)로 내리고
captured old association ID의 backend revoke만 bounded하게 시도할 수 있다.
현재 native subscription을 다시 조회해 unsubscribe하거나 association wildcard로
notification을 닫지 않는다. 정상 cleanup도 작업 시작 때 capture한 native
subscription, exact non-null association tag와 변경되지 않은 durable fence를
다시 확인한 경우에만 수행하며, 그 사이 새 association이 commit되면 모두
건너뛴다.
logout/account switch 자체는
account-neutral notification copy, click-time session 재인가와 짧은 server TTL에
의존하며, 복구 전 새 account에 기존 association을 재사용하지 않는다.
CONTROL_PURGE는 정상 revoke 뒤 자동 실행하지 않는다. 별도 retention/maintenance
owner가 exact REVOKED association을 정리할 때만 captured revision, authority와
association epoch를 모두 제시하고 repository revision CAS로 삭제한다. purge와
새 owner가 경합하면 STALE_REVISION으로 끝나며 새 control record를 지우지 않는다.
worker는 window의 in-memory session을 authority로 사용할 수 없다. Web Push를 선택하면 다음과 같은 최소 control record를 adapter-owned IndexedDB store에 보관한다.
type PushControlV1 = Readonly<{
protocol: "PUSH_CONTROL_V1";
fenceGeneration: string;
sessionBindingEpoch: string;
releaseEpoch: string;
association:
| Readonly<{ state: "UNASSOCIATED" }>
| Readonly<{
state: "ACTIVE" | "REVOKED";
associationEpoch: string;
}>;
updatedAt: string;
}>;
이 record에는 account/user ID, endpoint, p256dh, auth, notification content와
credential을 넣지 않는다. fenceGeneration은 session authority가 먼저 rotate하는
local opaque token이고 sessionBindingEpoch은 authenticated BFF가 발급한 opaque
exact-match 값이다. 둘 다 authorization proof나 readable subject ID가 아니다.
worker는 push hint의 association/release epoch와 exact match하고 association이
ACTIVE일 때만 처리한다. record가 missing/corrupt/future version이면
notification을 표시하지 않는다. 이 control store는 query persistence나 offline
data repository가 아니며 Web Push composition과 함께 설치·제거한다. open,
upgrade, transaction-complete, blocked/versionchange, corruption/eviction과
exact store purge는 VD-11/VD-15의 IndexedDB lifecycle을 재사용한다.
UNASSOCIATED는 첫 backend register 전에도 durable generation을 만들기 위한
명시적 상태다. register request는 이 record의 revision과 authority 세 필드를
capture한다. logout/account switch는 같은 record transaction에서 generation을
먼저 rotate하므로, 아직 associationEpoch을 받지 못한 old register callback도
CAS에 실패한다. backend가 발급하지 않은 sentinel association epoch을 만들거나
서로 다른 두 record의 비원자 갱신으로 이 race를 닫지 않는다.
같은 associationEpoch의 REVOKED는 terminal tombstone이며 ACTIVE로
overwrite할 수 없다. 새 ACTIVE는 backend가 commit 뒤 발급한 distinct epoch이고
current durable fenceGeneration/sessionBindingEpoch와 register request가
captured한 값이 exact match할 때만 허용한다. writer는 한 IndexedDB read-write
transaction 안에서 current state, expected record revision/association epoch,
durable fence generation, server session binding과 release epoch를 CAS 검증한다.
logout이 먼저 fence generation을 rotate했으면 old callback의 distinct
association epoch도 거절한다. updatedAt과 client wall clock은 ordering
authority가 아니다.
8.4 Push envelope와 worker 처리
Web Push payload는 foreground event envelope 전체를 옮기지 않는다.
type WebPushHintV1 = Readonly<{
protocol: "WEB_PUSH_HINT_V1";
notificationType: NotificationTypeId;
notificationId: string;
associationEpoch: string;
releaseEpoch: string;
issuedAt: string;
expiresAt: string;
routeIntent: NotificationRouteIntentId;
}>;
- application-owned hard byte cap을 push service/provider limit보다 작게 둔다.
- 개인 내용, message body, sender name, order amount 같은 민감한 본문을 기본 payload에 넣지 않는다.
notificationId는 foreground BFF가 현재 authorization으로 내용을 조회할 opaque reference다.- 최상위 JSON member name이 중복되면 JSON의 last-wins 해석을 허용하지 않고 payload를 거절한다.
issuedAt은 client clock보다 최대 5분 앞설 수 있고,expiresAt - issuedAt은 최대 24시간이다. 이 범위를 넘는 issue/expiry, association과 release mismatch는 drop한다.- unknown type/route/version과 malformed payload는 generic notification으로 downgrade하지 않고 fail-closed한다.
- payload가 없는 push를 사용할지 여부도 explicit registry policy다.
worker handler:
push event
-> event.waitUntil(bounded handler)
-> byte/schema/association/release/expiry validation
-> duplicate/collapse policy
-> registry-owned localized safe copy
-> registration.showNotification
Service Worker는 언제든 종료될 수 있다. in-memory cursor, queue, auth session이나
timer를 authority로 사용하지 않는다. waitUntil도 무제한 lifetime을 보장하지
않으므로 large fetch, retry loop, data migration을 넣지 않는다.
8.5 Notification content와 click
- title/body/icon/action은 closed notification registry가 만든다.
- backend raw text를 그대로 OS notification에 렌더링하지 않는다.
- 잠금 화면 노출을 고려해 default copy는 민감하지 않아야 한다.
tag/collapse는 notification type과 non-reversible association tag token으로 bounded하게 만든다. raw account/resource ID나 cache scope fingerprint를 쓰지 않는다.- action 수와 지원은 capability probe에 따라 degrade한다.
requireInteraction, sound/vibration과 높은 urgency를 기본값으로 사용하지 않는다.
notificationclick은 arbitrary URL을 열지 않는다.
type NotificationClickDataV1 = Readonly<{
protocol: "NOTIFICATION_CLICK_DATA_V1";
notificationId: string;
routeIntent: NotificationRouteIntentId;
associationEpoch: string;
releaseEpoch: string;
expiresAt: string;
}>;
worker가 종료·재시작된 뒤에도 click을 처리할 수 있도록 이 bounded, non-sensitive
envelope만 NotificationOptions.data에 넣는다. click handler는 codec/size/expiry,
current local ACTIVE fence와 release epoch를 다시 검증한다. unknown/mismatch는
route를 열지 않고 notification을 닫는다. 이미 표시된 notification은 logout 뒤
OS notification center에 남을 수 있으므로 copy는 항상 account-neutral하고
민감하지 않아야 하며, getNotifications() cleanup 성공을 privacy 보장으로
과장하지 않는다.
NotificationClickDataV1
-> codec/size/expiry + ACTIVE fence
-> closed route intent
-> route registry lookup
-> same-origin safe path encode
-> existing controlled client focus or openWindow
-> page boot/session recovery
-> authoritative notification/resource fetch
route intent가 stale/unknown이면 안전한 notification inbox/home으로 이동한다. notification action 자체에서 destructive 업무 mutation을 실행하려면 별도의 confirmed UI 또는 idempotent authorized server command 설계가 필요하다.
8.6 Provider와 browser evidence
- server-side subscription registration/revoke authorization
- VAPID signing key rotation과 private-key custody
- RFC 8291 payload encryption과 provider request conformance
- TTL, urgency, topic/collapse와 retry policy
- provider
404/410/429/5xxcleanup/retry - endpoint/key redaction과 data retention/deletion
- target browser 설치/permission/subscription/push/notification/click
- OS/browser notification setting change와 subscription rotation
- Service Worker update/rollback 중 handler compatibility
synthetic PushEvent unit test만으로 실제 provider delivery를 증명하지 않는다.
permission prompt와 OS notification은 자동화 가능 범위와 수동 evidence를
분리한다.
9. 제한된 Polling
9.1 허용하는 두 형태
- freshness polling: visible 화면의 server-state query를 낮은 빈도로 conditional refetch한다.
- convergence polling: 사용자가 시작한 async job이 terminal state에 도달할 때까지만 application orchestrator가 조회한다.
무한 setInterval, hidden tab polling, 여러 component의 중복 loop와 짧은
주기의 push emulation은 금지한다.
9.2 Poll lease
모든 polling은 immutable lease를 가진다.
type PollLeasePolicy = Readonly<{
operationId: ApiOperationId;
owner: QueryInvalidationTopic | UseCaseId;
minimumIntervalMs: number;
successIntervalMs: number;
maxIntervalMs: number;
maxAttempts: number;
maxElapsedMs: number;
maxResponseBytes: number;
visibility: "VISIBLE_ONLY";
fallbackReason: PollFallbackReason;
terminalStates: readonly PollTerminalState[];
}>;
implementation absolute ceiling보다 느슨한 값, zero interval, unlimited attempt,
unknown terminal state와 arbitrary operation ID는 startup에서 거절한다.
operationId는 existing API registry의 terminal QUERY, REST,
SAFE | IDEMPOTENT, non-SERVER_STREAM operation이어야 한다. command/mutation,
GraphQL subscription, Connect/gRPC-Web server stream과 arbitrary status URL은 poll하지
않는다. Poll-bound operation의 transport retry profile과 TanStack Query retry는
NONE/false여야 한다. 구체적으로 VD-23 LogicalExecutionBudget은
maxAttempts=1, authRecoveryCount=0, maxCumulativeSleepMs=0으로 고정해
executor 내부 status retry나 401 replay가 일어나지 않게 한다.
한 lease는 다음 조건 중 먼저 발생하는 시점에 종료한다.
- terminal state
- max attempts
- max elapsed
- request/response budget
- route unmount 또는 consumer 0
- hidden/pagehide
- offline
- session/account/release generation change
- authorization required/forbidden
- user cancel
- primary stream recovery로 fallback이 더 이상 필요 없음
종료 뒤 자동으로 새 lease를 만들지 않는다. visible 복귀, user retry 또는 coordinator의 명시된 state transition이 새 lease를 발급한다.
9.3 Scheduling
lease admitted
-> wait jittered cadence
-> verify visible/online/scope/generation
-> start exactly one request with AbortSignal
-> validate bounded response
-> terminal? close lease
-> unchanged? success cadence
-> retryable failure? capped backoff/Retry-After
-> budget remaining? next attempt
-> otherwise stale/degraded
setTimeoutcompletion chaining으로 single-flight를 보장한다.- interval tick이 이전 request와 겹치지 않는다.
- success cadence와 failure backoff를 구분한다.
- Poll
maxAttempts의 한 attempt는 registered REST operation의 한 logical completion이자 한 physical request다. credential recovery, decode와 mapping 시간도 leasemaxElapsed에 포함하며 transport 내부 sleep/replay로 lease budget을 우회하지 않는다. 401은 해당 attempt를 끝낸다. session owner가 recovery한 뒤 요청을 다시 보내면 새 Poll attempt/physical request로 계산한다.Retry-After는 local backoff보다 빠르게 만들지 않으며 maximum wait/lease budget을 넘으면 낮춰서 더 일찍 요청하지 않고 stale UI로 종료한다.- immediate manual refresh가 실행되면 pending poll을 취소하거나 같은 Query execution을 공유한다.
- SSE/WebSocket reconnect와 fallback polling이 동시에 authoritative refetch를 소유하지 않도록 generation 하나만 active owner가 된다.
9.4 HTTP contract
freshness polling은 가능하면 conditional request를 사용한다.
- server-issued ETag +
If-None-Match 304는 representation unchanged이며 response body가 없어야 한다.- weak/strong validator 의미는 endpoint owner가 정한다.
Cache-Control과 Query stale semantics를 혼동하지 않는다.- cursor/since endpoint를 사용하면 cursor expiry와 full snapshot reset을 명시한다.
401/403/404/409/410/429/5xx를 closed failure로 mapping한다.429/503의 boundedRetry-After를 존중한다.- response byte와 decode deadline을 기존 HTTP operation registry가 제한한다.
304라도 current account scope와 lease generation이 바뀌었으면 결과를
적용하지 않는다.
9.5 Polling을 fallback으로 사용할 때
SSE/WS 장애에서 Polling으로 전환하려면 다음이 모두 참이어야 한다.
- 같은 logical resource/freshness 의미를 snapshot endpoint가 제공한다.
- duplex-only interaction은 명시적으로 disabled UI가 된다.
- fallback transition이 새로운 generation을 발급한다.
- common
RecoveryCoordinator가 아래 handoff state를 단독 소유한다. POLL_ACTIVE에서는 poll만 projection effect writer이고 live candidate는 bounded health/checkpoint probe와 post-checkpoint buffer만 수행한다.- active writer의 sequential effect queue도 in-flight 항목을 포함해 256건/4MiB
중 먼저 도달하는 고정 상한을 예약한다. 비협조적인 첫 effect가 tail을 막아도
초과 요청은 즉시
QUEUE_OVERFLOW로 전체 generation을 fence/abort한다. - candidate의 current authorization/checkpoint를 검증한 뒤 handoff mutex를 잡고, poll generation을 fence/abort하며 in-flight poll quiescence를 기다린다.
- 그 뒤에만 current-generation snapshot/checkpoint를 projection에 적용하고 buffered post-checkpoint event를 drain한 뒤 live effect를 활성화한다.
- 어느 단계든 실패하면 candidate를 폐기하고 새 poll generation을 발급하거나 stale/manual UX로 종료한다.
- UI와 diagnostics가
LIVE와POLLING/STALE를 구분한다.
LIVE_ACTIVE
-> LIVE_DEGRADED
-> POLL_ACTIVE
-> LIVE_PROBING
-> handoff mutex
-> fence/abort poll + await quiescence
-> current-generation snapshot/checkpoint apply
-> buffered post-checkpoint event drain
-> LIVE_ACTIVE
fallback을 사용해 provider 장애를 production 정상으로 숨기지 않는다.
freshness SLO를 충족하지 못하면 DEGRADED다.
10. Query cache와 application effect
10.1 기본 invalidation 흐름
validated external event
-> feature event input
-> INVALIDATE(topic)
-> presentation query bridge
-> current scope의 active query invalidate/refetch
-> server authorization + schema + mapper
-> cache update
event handler가 raw QueryClient, query key array나 native transport를 직접
받지 않는다. query namespace와 key codec은 client-cache registry가 소유한다.
event burst에서 같은 namespace invalidation은 bounded window 안에서 coalesce할 수 있다. 이 최적화는 last cursor 적용 순서와 gap detection을 바꾸지 않는다.
10.2 Authoritative delta를 허용하는 조건
다음이 모두 있을 때만 event payload로 cache projection을 직접 갱신한다.
- event type별 exact payload codec
- server commit 뒤 발행 증거
- entity/collection의 base revision과 resulting revision
- 동일 revision의 idempotent reducer
- gap/out-of-order/epoch reset 처리
- 현재 query filter/sort/pagination에 미치는 의미
- account scope/generation fence
- snapshot reconciliation test
하나라도 없으면 invalidation/refetch를 사용한다. partial patch를 모든 list/detail cache에 추측해서 적용하지 않는다.
10.3 External event와 business command
SSE/WebSocket/Push handler는 application input을 호출할 수 있지만, 외부 event를 사용자 command와 같은 authorization으로 취급하지 않는다. server가 이미 commit한 사실을 projection에 반영하거나 user-visible notification을 만드는 입력이다.
WebSocket client command를 선택해도 optimistic state, ack, server commit, event echo와 HTTP refetch의 순서를 별도 protocol로 정의한다. ack만으로 authoritative query data를 덮지 않는다.
11. Resource budget와 backpressure
다음 값은 target reference runtime의 절대 상한이다. 제품 registry는 더 작게 설정할 수 있지만 높일 수 없다. 실제 구현 branch는 bundle/browser 측정으로 값을 검토하고 변경 시 ADR amendment를 남긴다.
| 자원 | target implementation ceiling |
|---|---|
| physical foreground connection | runtime당 primary 1, drain handoff 포함 일시적 2 |
| WebSocket logical subscriptions | runtime당 32; SSE baseline은 feed 1 |
| decoded foreground event/frame | 64 KiB |
| SSE incomplete parser buffer | 128 KiB |
| inbound apply queue | 256 event 또는 4 MiB 중 먼저 도달 |
| live↔Poll active effect/probe queue | 각각 256 event 또는 4 MiB 중 먼저 도달 |
| dedupe window | stream당 2,048 ID 또는 10분 중 먼저 만료 |
| reorder buffer | 기본 0; 선택 시 64 event, 2초 |
| WebSocket outbound queued bytes | 256 KiB |
| WebSocket outbound queued messages | 128 |
| reconnect attempts | 연속 10회 또는 5분 |
| reconnect max delay | 60초 |
| aborted reconnect task drain | 2초 고정; 구현 절대 최대 30초 |
| transport recovery readiness/barrier gate | attempt당 30초 |
| live↔Poll handoff quiescence/checkpoint | 단계당 30초 |
| poll minimum interval | 5초 |
| poll lease | 120회 또는 30분 |
| Web Push decoded application hint | 3 KiB |
| Web Push hint future clock skew | 5분 |
| Web Push hint maximum lifetime | 24시간 |
| worker push/click/subscription-change handler application deadline | 10초 |
| window native permission/subscription operation deadline | operation당 30초 |
| backend register/reconcile/revoke operation deadline | operation당 15초 |
| Push association fence transaction | operation당 2초, open/transaction 한 번 |
| notification cleanup scan | association당 64개 또는 2초 |
이 표의 reference ceiling은 deterministic runtime의 fail-closed 경계로
구현됐다. 실제 backend/provider/target-browser evidence와 제품별 ceiling 승인은
아직 pending이며, 그 전에는 AVAILABLE_NOT_COMPOSED를 넘지 않는다.
reconnect drain 상한은 offline/abort로 phase가 이미 중단된 뒤의
sleep/connect attempt/closed-receipt cleanup에만 적용한다. 정상 active
session의 waitClosed에는 deadline을 두지 않는다. 2초 안에 정리되지 않은
task는 run을 fail-closed로 끝내되 underlying task가 settle할 때까지 lifecycle을
DRAINING으로 유지하여 다음 physical connection을 허용하지 않는다.
미선택 reference source 전체를 tree-shaking 없이 합성하는 optional-recipe 측정의 gzip 상한은 40,000 bytes다. 이 값은 production bundle 허용량이 아니라 RT-01~RT-04 reference runtime의 회귀 예산이며, production asset에는 계속 미선택 realtime module이 0이어야 한다.
ceiling 도달은 다음처럼 fail-closed한다.
- oversized event/frame/push: protocol rejection, apply 금지
- inbound queue: freshness
UNKNOWN, connection close와 snapshot resync - outbound queue: typed backpressure rejection, durable command enqueue 금지
- reconnect/poll budget: degraded/stale UI, timer 종료
- subscription count: 새 lease 거절
- worker deadline: bounded handler 종료, 무제한 retry 금지
event rate가 지속적으로 ceiling 근처라면 limit을 자동으로 높이지 않고 snapshot, aggregation, server coalescing 또는 다른 protocol을 재설계한다.
12. 인증, 보안과 privacy
12.1 인증
- HTTPS/WSS만 허용한다.
- endpoint는 composition registry의 fixed URL이다.
- credential은 external session owner와 transport adapter 안에 남는다.
- URL/query, WebSocket subprotocol, event/push payload, cursor, storage와 telemetry에 credential을 넣지 않는다.
- connect 성공 뒤에도 logical subscribe/command와 snapshot을 server가 각각 authorization한다.
- logout/account switch는 local generation fence와 server association/session revoke를 모두 수행한다.
- route guard, event scope binding과 push permission은 authorization proof가 아니다.
native EventSource/WebSocket의 custom-header 제약 때문에 bearer token을 query로 옮기지 않는다. same-origin BFF/cookie 또는 보안 검토를 통과한 별도 handshake mechanism을 선택한다.
12.2 Input validation과 abuse control
- raw bytes → strict schema → mapper 순서를 지킨다.
- unknown field/version/event/message/close category는 fail-closed한다.
- object depth, array/property/string count, decoded bytes와 event rate를 제한한다.
- WebSocket decompressed size와 outbound
bufferedAmount를 제한한다. - SSE/WS endpoint는 origin, connection count, subscription count와 per-subject rate limit을 server에서 강제한다.
- push provider는 VAPID, encrypted payload, TTL과 send-time authorization을 강제한다.
- notification click route는 same-origin closed registry만 허용한다.
- CSP
connect-src, worker/source 정책과 hosting security header를 실제 deployment에서 검증한다.
12.3 금지하는 관측·저장 값
- raw endpoint URL/query
- credential/cookie/token
- account/tenant/user ID와 email
- event ID, resume cursor, stream epoch와 scope binding 원문
- raw event/frame/push payload
- WebSocket raw close reason
- PushSubscription endpoint,
p256dh,auth - notification private content
- server/backend exception text
허용 속성은 registry ID, transport kind, closed failure/category, attempt/count, queue/lag/duration bucket과 low-cardinality environment ID다.
NotificationOptions.data에는 §8.5의 bounded
NotificationClickDataV1만 예외적으로 저장할 수 있다. 이것은 opaque,
non-sensitive click envelope이며 credential, account/resource ID와 자유 문구를
추가하는 예외가 아니다.
fenceGeneration과 sessionBindingEpoch은 §8.3의 adapter-owned IndexedDB
control record 안에서만 저장할 수 있다. generic storage, URL,
BroadcastChannel/telemetry로 복사하지 않는다. foreground cursor/scope binding은
bounded adapter memory에만 존재하고 document restore에서는 authoritative
checkpoint를 다시 얻는다.
client-side encryption만으로 같은 runtime의 XSS에서 push endpoint, cached payload나 credential을 보호할 수 있다고 주장하지 않는다.
13. Closed failure와 recovery
13.1 Failure vocabulary
| failure | 의미 | 기본 recovery |
|---|---|---|
ABORTED |
lifecycle/user cancellation | retry 없음 |
UNSUPPORTED |
browser capability 없음 | declared fallback |
OFFLINE |
network hint/attempt failure | online hint 뒤 bounded check |
CONNECT_TIMEOUT |
open deadline 초과 | bounded reconnect |
IDLE_TIMEOUT |
heartbeat/byte deadline 초과 | close + bounded reconnect |
AUTH_REQUIRED |
session expired/recovery 실패 | sign-in UX |
FORBIDDEN |
stream/resource 거절 | retry 없음 |
RATE_LIMITED |
connection/request/provider limit | bounded server hint |
PROVIDER_UNAVAILABLE |
backend/push/hosting unavailable | bounded retry 또는 degraded |
PROTOCOL_MISMATCH |
version/subprotocol/content type | terminal/rollout rollback |
MALFORMED_EVENT |
state-bearing syntax/schema/exact-key 실패 | cursor 미진행, freshness UNKNOWN, close + registered recovery |
MAPPING_CONTRACT_VIOLATION |
validated DTO의 registered mapper 실패 | effect/cache/cursor write 0, close + registered recovery |
EVENT_CONFLICT |
같은 ID/sequence가 다른 event를 가리킴 | terminal protocol conflict + resync/rollback |
EVENT_TOO_LARGE |
byte ceiling 초과 | close/resync, retry storm 금지 |
DUPLICATE_EVENT |
이미 처리 | safe ignore |
STALE_EVENT |
old/out-of-order | safe ignore + 관측 |
SEQUENCE_GAP |
missing event | snapshot resync |
CURSOR_EXPIRED |
replay retention 밖 | snapshot reset |
QUEUE_OVERFLOW |
backpressure ceiling | close + registered snapshot/session rebuild |
APPLY_FAILED |
feature effect 실패 | cursor 미commit, bounded registered recovery |
POLL_BUDGET_EXHAUSTED |
lease 한도 도달 | stale/manual retry |
PUSH_PERMISSION_DENIED |
사용자 거절 | foreground fallback |
PUSH_SUBSCRIPTION_STALE |
rotation/expiry/provider gone | bounded re-registration |
NOTIFICATION_REJECTED |
invalid/expired/scope mismatch | safe drop |
SCOPE_FENCED |
old account/release generation | safe drop |
SCOPE_PROTOCOL_VIOLATION |
current connection event의 scope binding mismatch | effect/cursor 0, close + session revalidation/snapshot + security diagnostic |
CLOSED |
terminal runtime | 새 generation만 생성 가능 |
raw native error, close reason이나 backend message를 이 union에 추가하지 않는다.
safe user copy는 i18n message catalog가 failure kind를 mapping한다.
drop-only malformed 처리는 명시적으로 non-state-bearing heartbeat/control frame에만
허용하며, threshold를 넘으면 protocol close한다. state-bearing event나 unknown
event type을 버리고 freshness를 CURRENT로 유지하지 않는다.
13.2 Effect certainty
| effect | certainty |
|---|---|
socket send() returned |
local API accepted bytes; server receipt 미확인 |
| WebSocket command ack | protocol-defined acceptance; business commit은 별도 |
| SSE/WS event decoded | wire valid; application effect 미commit |
| CURSOR advanced | local effect 이후 resume position commit |
| query invalidated | local cache action accepted; refetch/server commit 미확인 |
Poll 304 |
selected representation unchanged |
| push provider accepted | provider queue accepted; browser/user delivery 미확인 |
| Service Worker push fired | browser handler invoked; notification shown/clicked 미확인 |
showNotification() resolved |
platform show step accepted; user 확인 미확인 |
| backend subscription revoke success | 해당 association의 server revoke commit |
diagnostics와 UI는 이 의미보다 강한 “전달됨”, “읽음”, “동기화 완료”를 표시하지 않는다.
14. Observability
14.1 Closed diagnostic events
foreground:
realtime_runtime_state_changedrealtime_connect_attemptedrealtime_connect_finishedrealtime_connection_closedrealtime_heartbeat_observedrealtime_event_receivedrealtime_event_rejectedrealtime_event_appliedrealtime_gap_detectedrealtime_resync_finishedrealtime_queue_pressurerealtime_fallback_changed
polling:
bounded_poll_lease_startedbounded_poll_attempt_finishedbounded_poll_unchangedbounded_poll_rate_limitedbounded_poll_lease_stopped
push:
web_push_permission_finishedweb_push_registration_finishedweb_push_subscription_rotatedweb_push_hint_processedweb_push_notification_finishedweb_push_click_dispatchedweb_push_association_revoked
모든 event는 semantic registry에 등록하고 arbitrary payload를 받지 않는다. diagnostics sink failure는 connection, worker handler, poll lease와 cleanup을 실패시키지 않으며 재귀 telemetry를 만들지 않는다.
14.2 Metrics와 SLO
- connect success/latency와 visible open uptime
- reconnect attempts, delay와 exhausted ratio
- heartbeat age/timeout
- accepted/rejected/duplicate/stale/gap event count
- event receive-to-apply lag bucket
- resync count, latency와 failure
- active physical connection/logical subscription
- queue high-water count/bytes와 overflow
- poll request,
304, error, stop reason과 requests per lease - push permission outcome
- active/revoked/stale subscription aggregate
- provider accepted/gone/rate-limited aggregate
- worker handler/notification/click aggregate
- fallback/degraded/stale duration
event timestamp와 client clock 차이를 정확한 network latency로 해석하지 않는다. server/client clock skew가 있으므로 protocol ingest/apply monotonic duration과 server-side metric을 함께 사용한다.
15. Test와 promotion evidence
15.1 Pure unit/property
- transport selection matrix와 forbidden combination
- exact registry/config freeze와 ceiling
- decimal sequence boundary와 unsafe input
- duplicate/stale/gap/epoch reset
- CURSOR/non-CURSOR discriminated codec와 conditional cursor commit
- effect failure 뒤 cursor 미commit
- deterministic fake clock/random의 full-jitter backoff
- stable-open 이후에만 retry attempt reset
- queue/dedupe/reorder ceiling
- old-generation late callback drop와 current scope-binding protocol violation 분리
- effect/recovery callback lease의
isCurrent()commit 직전 확인과 callback 종료 뒤 영구 만료 - 외부 result/recovery commit의 own data descriptor 단일 snapshot, accessor, Proxy 재조회, extra/inherited/symbol field 거절
- state-axis와 UI projection
- Poll lease terminal/budget/visibility
- notification route/copy registry
15.2 Deterministic protocol/fault contract
SSE:
- BOM, CR/LF/CRLF, comment, multi-line data와 incomplete EOF
- wrong status/content type, redirect, oversized line/event
- CURSOR direct
id/Last-Event-ID equality와 non-CURSOR id 부재/null cursor - disconnect before/after effect commit
204, auth, rate limit, cursor reset- native EventSource snapshot gate 중 pending invalidation drain
- exact recovery proof, readiness confirmation과 clone/missing/deadline failure
WebSocket:
- wrong/missing subprotocol
- text/binary/unknown frame
- welcome/subscribe/event/reset/heartbeat/close order
- frame/decompressed/queue/outbound buffer ceiling
- duplicate/out-of-order/gap and reconnect resume
- accepted cursor/next sequence silent advance와 initial snapshot barrier rejection
- unsubscribe tombstone, matching
UNSUBSCRIBED, late frame drop, ACK deadline과 32회 slot 회수 - negotiated outbound count/bytes/
bufferedAmountFIFO overflow의 terminal close - close category와 auth recovery
Polling:
200/304/401/403/409/410/429/503- ETag/cursor,
Retry-After, single-flight와 no-overlap - transport max attempt 1/auth recovery 0과 401 새 Poll attempt
- max attempts/elapsed/response bytes
- hidden/offline/unmount/cancel/terminal stop
- handoff mutex, poll quiescence, snapshot/buffer ordering과 failure rollback
- non-cooperative active writer와 256건/4MiB effect queue overflow fail-close
Web Push:
- permission states
- subscription register/revoke/rotation
- endpoint/key redaction
- association fence IDB open/transaction/blocked/versionchange/missing/corrupt/eviction
- old fence generation의 distinct-epoch stale ACTIVE와 same-epoch
REVOKEDtombstone resurrection 거절 - exact revoked association purge와 concurrent newer-owner revision CAS
- malformed/oversized/expired/wrong-scope hint
- worker
waitUntil, handler deadline,pushsubscriptionchangelifecycle abort와 non-cooperative client enumeration - safe localized notification, persisted typed click route와 old-tag cleanup
- declarative
web_push: 8030payload rejection - provider
404/410/429/5xxmapping
15.3 Local integration server
MSW/jsdom만으로 streaming handshake, proxy flush, socket close와 Service Worker lifecycle를 증명하지 않는다. test 전용 실제 local server를 사용해 다음을 검증한다.
- chunked SSE flush, heartbeat, abort, reconnect와 cursor replay
- WebSocket upgrade/subprotocol, server restart, heartbeat, burst와 close
- auth expiry/recovery와 scope switch
- replay retention/gap/snapshot reset
- conditional Polling과 rate limit
- provider adapter는 별도 conformance harness로 동일 scenario를 실행
15.4 실제 browser
승인된 Chromium, Firefox, WebKit matrix에서 built production asset으로 확인한다.
- route mount/unmount와 React StrictMode 후 connection/listener/timer leak 0
- offline/online, hidden/visible, pagehide/pageshow와 bfcache
- logout/account switch/release transition의 late event fence
- SSE stream parsing/abort와 hosting-equivalent proxy
- WebSocket buffered queue/close/reconnect
- Poll single-flight와 hidden stop
- Service Worker registration/update, push event, notification/click
- permission denied/default/unsupported UX
- CSP와 cross-origin rejection
Web Push actual delivery는 browser automation, OS/manual evidence와 provider receipt를 분리한다. 한 engine의 synthetic event를 세 browser/provider 증거로 재사용하지 않는다.
15.5 Load, chaos와 security negative gate
- event burst와 sustained rate에서 memory/queue ceiling
- disconnect/restart/redeploy/replay-store unavailable
- effect commit 직전/직후 crash
- reconnect herd와 server hint
- multi-tab connection budget
- provider push burst/expired endpoint
- polling rate-limit과 backend slowdown
static/security gate는 다음을 거절해야 한다.
- adapter/composition 밖의
new EventSource,new WebSocket,pushManager.subscribe - application/domain의 native/vendor type import
- arbitrary endpoint/channel/topic/send payload
- token/cursor/PushSubscription material의 URL/storage/telemetry/BroadcastChannel
- raw event/push/close reason logging
- declarative push payload/handler bypass before a separate selection ADR
- page component
setIntervalpolling - 선택 전 production bootstrap/Service Worker composition
15.6 Promotion evidence
COMPOSED와 production traffic approval은 별도다. 최소 component evidence:
| component | 필요 증거 |
|---|---|
| contract | frontend/backend exact wire, cursor/reset/auth/failure agreement |
| provider | SSE hosting 또는 WS gateway 또는 Push provider conformance |
| browser | target matrix의 lifecycle/permission/stream evidence |
| operations | dashboards, alert, kill switch, drain/recovery/rollback drill |
| security/privacy | threat model, endpoint/credential/payload redaction, retention approval |
| performance | connection/request/event/push rate와 memory/battery budget |
필수 외부 증거가 없으면 PromotionEvidence는 MISSING | PARTIAL에 머물고
promotion gate result는 FAIL_UNVERIFIED다. fake 또는 local server로 provider
성공을 대신하지 않는다.
16. Optional composition, rollout과 rollback
16.1 Runtime composition result
선택된 runtime factory는 partial object를 외부에 반환하지 않는다.
READY {
generation,
selected transport,
logical subscription facade,
freshness snapshot,
close()
}
UNAVAILABLE {
safe reason,
supported fallback,
cleanup outcome
}
생성 중 실패하면 factory가 finally에서 열린 stream/socket, readers, timers,
lifecycle listeners와 subscription draft를 역순으로 정리한 뒤 결과를 반환한다.
caller에게 partial/native handle이나 cleanup callback을 넘기지 않는다.
Web Push는 별도 result다.
PUSH_READY | PUSH_PERMISSION_REQUIRED | PUSH_DENIED |
PUSH_UNSUPPORTED | PUSH_UNAVAILABLE
foreground realtime이 준비됐다는 이유로 push도 준비됐다고 표시하지 않는다.
16.2 Traffic admission
DISABLED
-> SHADOW
-> CANARY
-> ENABLED
SHADOW | CANARY | ENABLED
-> DISABLED
SHADOW는 production event가 아닌 승인된 probe/synthetic stream만 사용한다.CANARY는 subject/session hash가 아닌 server-side allowlist로 제한한다.- transport, event stream, Poll fallback과 Web Push category는 독립 kill switch를 가진다.
- kill switch는 config fetch 실패 시 safe default
DISABLED다. - admission이
DISABLED가 되면 connection lifecycle을DRAINING으로 옮겨 새 lease/send/subscription을 막고 active operation을 bounded close한 뒤CLOSED로 전환한다.
rollback:
- 새 traffic admission을 닫는다.
- active connection/poll/push registration 작업을 drain한다.
- stale/focus/manual refresh fallback을 노출한다.
- backend publisher/replay/subscription compatibility window를 유지한다.
- frontend composition과 dependency를 제거한다.
- production bundle/worker에서 source 부재를 검증한다.
SSE에서 WebSocket으로, 또는 그 반대로 즉석 rollback하지 않는다. protocol semantics가 다른 경우 기존 HTTP/stale fallback으로 먼저 안전하게 내린다.
17. 제거 가능성
17.1 제거 순서
foreground:
- traffic admission
DISABLED, connection lifecycleDRAINING - logical subscription revoke
- Poll lease와 reconnect/backoff timer abort
- SSE reader/EventSource 또는 WebSocket close
- lifecycle/online listener 제거
- cursor/dedupe/queue memory 폐기
- composition/registry 제거
- adapter, port, test와 dependency 제거
Web Push:
- 새 opt-in과 server send admission 중지
- durable fence generation rotate + local
REVOKED를 같은 transaction에 commit - backend account association revoke
- owned notification bounded close와 native unsubscribe policy 실행
- association fence store exact purge
- push/notification handler를 worker에서 제거
- worker update/drain과 old client 호환 확인
- notification registry, provider adapter와 key config 제거
- retained subscription data를 server retention 정책대로 삭제
17.2 Removal gate
미선택/제거 상태에서 다음이 0이어야 한다.
- production bundle의 optional realtime/vendor sentinel
- runtime connect/subscribe/poll timer side effect
- production Service Worker push listener와 subscription request
- runtime config의 orphan endpoint/topic/key
- CSP의 불필요한 realtime/provider origin
- dependency/SBOM의 제거 대상 package
- docs/catalog의
INSTALLED주장
base typecheck, architecture, unit/integration, production build, optional recipe gate와 module inventory를 다시 실행한다. stale server subscription/replay topic과 provider key는 frontend source 삭제만으로 제거됐다고 주장하지 않는다.
18. Target source 배치
실제 선택 branch의 예시다. 제품 protocol에 따라 더 좁혀야 하며 지금 이 source가 있다는 뜻은 아니다.
src/
application/
ports/
in/
<feature>-external-event-input.ts
out/
live-subscription-control.ts
<feature>-presence-gateway.ts
policies/
external-event-effect.ts
bounded-polling.ts
contracts/
realtime-streams.ts
realtime-events.ts
notification-types.ts
adapters/
realtime/
event-codec.ts
stream-coordinator.ts
sse/
fetch-sse-connection.ts
sse-parser.ts
websocket/
websocket-connection.ts
websocket-protocol.ts
polling/
bounded-poll-coordinator.ts
web-push/
push-subscription-adapter.ts
push-registration-gateway.ts
push-association-fence-store.ts
inbound/
push-event-adapter.ts
notification-click-adapter.ts
bootstrap/
realtime-runtime-composition.ts
service-worker/
service-worker-composition.ts
presentation/
adapters/
query/
external-event-query-bridge.ts
tests/
unit/realtime/
integration/realtime/
browser-capabilities/realtime/
generic protocol mechanism은 src/adapters/realtime에 둘 수 있지만 feature event
codec/input과 query namespace는 feature owner가 소유한다. Service Worker entry는
window bootstrap을 import하지 않으며 둘이 공유하는 contract는 browser-neutral
module이어야 한다. bootstrap/service-worker는 factory/handler registration만
조립하고 push/click decode와 fence storage 구현은 inbound/infrastructure
adapter에 둔다.
19. 구현 work package
RT-00 — 계약과 상태
- VD-28과 상세 설계 승인
- current status와 readiness 축 고정
- 기존 recipe의 한계 기록
- backend/hosting/provider owner와 protocol draft 지정
initial exit: 구현 없음, 모든 runtime은 DESIGNED_NOT_IMPLEMENTED. 현재는
RT-01~RT-04가 AVAILABLE_NOT_COMPOSED이고 제품 protocol/owner 선택은 RT-05에
남아 있다.
RT-01 — 공통 event authority
- closed stream/event registry
- target envelope codec
- scope/generation fence
- sequential queue, dedupe/order/gap/cursor commit
- authoritative snapshot reset facade
- bounded reconnect owner와 authoritative close classification
- deterministic failure/observation contract
exit: transport-independent contract/fault suite 통과.
RT-02 — SSE와 bounded polling
- bounded fetch-stream SSE parser/adapter
- same-origin auth와 fixed endpoint
- reconnect/heartbeat/status/resume
- single-flight visible-only Poll lease
- single-writer live↔Poll handoff와 bounded checkpoint/quiescence
- local streaming server와 target browser evidence
- removal/bundle gate
exit: reference runtime AVAILABLE_NOT_COMPOSED. 제품 선택은 계속
NOT_SELECTED.
RT-03 — WebSocket
- fixed handshake/subprotocol
- closed control frames
- app heartbeat, close mapping과 resume
- bounded inbound/outbound queue
- local WS server/load/browser evidence
- removal/bundle gate
exit: reference runtime AVAILABLE_NOT_COMPOSED. duplex product requirement가
없으면 composition하지 않는다.
RT-04 — Web Push
- permission/subscription facade
- fixed backend registration/revoke adapter
- Service Worker composition과 strict hint codec
- safe notification/click registry
- provider/browser/manual evidence
- subscription rotation/logout/removal
exit: reference runtime과 worker가 AVAILABLE_NOT_COMPOSED; actual provider
evidence 없이 product promotion 금지.
RT-05 — 제품 composition과 운영
- 제품 event/query/notification registry
- backend replay/outbox/snapshot/provider conformance
- immutable runtime config와 kill switch
- canary SLO/load/security/privacy review
- operations recovery/rollback drill
exit: 선택 capability만 COMPOSED, 별도 promotion evidence가 COMPLETE일 때만
production traffic 승인.
20. 완료 기준
설계 완료
- SSE, WebSocket, Web Push와 Polling의 전달 의미를 분리했다.
- 현재 recipe와 concrete runtime 상태를 구분했다.
- outbound connection과 inbound event adapter 경계를 정했다.
- source of truth, duplicate-tolerant CURSOR 처리, best-effort profile과 snapshot resync를 정했다.
- target envelope, scope/generation/cursor 의미를 정했다.
- lifecycle, retry owner, resource ceiling과 closed failure를 정했다.
- permission/subscription/worker/notification 경계를 정했다.
- test, promotion, rollout, rollback과 제거 조건을 정했다.
Reference runtime 완료
- RT-01~RT-04 source와 deterministic test가 있다.
- actual SSE/WS local server와 target browser evidence가 있다.
- Service Worker/Push provider evidence가 자동/수동 범위별로 있다.
- static boundary/security fixture, synthetic bundle budget와 runtime-removal 검증이 blocking gate다.
- provider/browser promotion evidence와 operations drill이 release blocking으로 등록됐다.
- static source gate 기준 production bootstrap과 worker에서 미선택 capability side effect가 0이다.
제품 composition 완료
- 제품 freshness/interaction/notification SLO와 owner가 승인됐다.
- event/topic/query/notification registry가 닫혀 있다.
- backend commit/replay/snapshot/auth/provider 계약이 conformant다.
- session/account/release transition과 late callback fence가 browser에서 검증됐다.
- dashboards, alert, kill switch와 recovery/rollback drill이 있다.
- privacy/retention, permission UX와 security review가 승인됐다.
Selection=SELECTED, 선택 transport의 runtime만COMPOSED다.PromotionEvidence=COMPLETE전에는 production-ready를 주장하지 않는다.
21. 관련 자료
저장소 내부:
- VD-28
- VD-10 optional capability recipes
- Frontend ports, adapters, and boundaries
- Optional frontend adapter recipes
- Client cache and browser storage
- API contract, Schema, Mapper와 Server State
- Browser file and origin-storage platform
- TypeScript, state, and data flow
RealtimePortcopyable recipe- Deterministic realtime fake
- Optional capability contract test
- Machine-readable optional recipe catalog
공식 기준:
- WHATWG Server-sent events
- WHATWG WebSockets
- RFC 6455 — The WebSocket Protocol
- W3C Push API
- RFC 8030 — Generic Event Delivery Using HTTP Push
- RFC 8291 — Message Encryption for Web Push
- RFC 8292 — VAPID for Web Push
- WHATWG Notifications API
- W3C Service Workers
- RFC 9110 — HTTP Semantics
- RFC 6585 — Additional HTTP Status Codes