Files
tech-log-frontend/docs/architecture/realtime-events-web-push-and-bounded-polling.md
T
DongHyeonkaandClaude Opus 5 4bff9ca151 chore: sync the frontend template from 4dc033c to 8157ad4
The product was materialized from the template at `4dc033c` and has stayed
on it through 43 template commits, so it was missing all three rounds of
adapter remediation — including files it never had, such as the shared
`abortable-operation` primitive and the `exact-snapshot` decoder that
later fixes are written against. Taking only the newest round was not
possible for that reason: the delta is coherent only as a whole.

The product had not touched `src/adapters` at all since materialization,
so the 140-file delta applied with a three-way merge and no conflicts.
`package.json` was the single overlap and merged cleanly: the product owns
`name`, the template contributed `check:adapter-inventory`,
`check:remediation-ledger` and the image-resolve-signal type fixture.
All 24 product-owned files — README, index.html, CI workflow, i18n
catalog, home page, generated schemas, evidence scripts, component and
visual snapshots — are byte-identical to `main`.

`template.lock.json` now pins the synced revision and tree.

Verified in this repository, not inherited from the template: six type
projects, lint, nine gates (adapter inventory, remediation ledger,
registries, diagnostics, realtime boundaries, architecture, browser
file/storage boundaries, optional recipes, documentation), the production
build, and 2,054 of 2,073 tests. The 19 failures are all in
`tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing
sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template
records; four suites that failed once under parallel load pass in
isolation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 12:04:58 +09:00

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 성공을 증명하지 않는다.
  • CURSOR profile의 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_V1 JSON 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_STREAMCONNECT_HTTP | GRPC_WEB + rpcKind=SERVER_STREAM만 허용한다. SSE, WebSocket과 Web Push는 realtime registry가 소유하고, bounded polling은 terminal·replay-safe REST QUERY operation에 적용하는 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한다.
  • streamIdeventType은 registry에 존재해야 한다.
  • streamEpoch은 server stream reset, partition rebuild 또는 호환 불가능한 replay change 때 바뀌는 opaque identifier다.
  • eventId는 bounded dedupe용 opaque ID다.
  • sequence는 stream + epoch 안에서만 단조 증가하는 canonical unsigned decimal string이다. JSON number의 safe-integer 한계를 피하고 parse 뒤 bounded integer representation으로 비교한다.
  • CURSOR의 non-empty resumeCursor만 server-issued opaque replay position이다. event ID나 sequence와 같은 문자열일 수 있어도 의미는 별도다.
  • SNAPSHOT_ONLY | SESSION_REBUILD는 cursor를 발급·전송하지 않고 exact resumeCursor: 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로 다시 인가한다. CURSORprotocol + 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 | nullnextExpectedSequence를 반환한다. 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, SSE retry와 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 EventSource constructor는 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_ONLY recovery 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과 normalized text/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-empty id를 직접 포함하고 decoded envelope의 resumeCursor와 exact match해야 한다. 이전 block의 inherited SSE ID만으로 통과시키지 않는다. SNAPSHOT_ONLY | SESSION_REBUILDid를 발급하지 않고 envelope cursor도 exact null이어야 한다.
  • 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 CURSOR resume이면 nextExpectedSequence는 반드시 local lastAppliedSequence + 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은 UNSUBSCRIBING tombstone으로 남긴다. matching UNSUBSCRIBED ACK 전에는 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 COMMAND operation이다. 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와 provider 404/410 cleanup을 함께 처리한다.
  • pushsubscriptionchange worker 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:

  1. durable fenceGeneration을 먼저 rotate하고 association을 REVOKED로 같은 transaction에 commit한다.
  2. window의 old runtime generation을 fence한다.
  3. authenticated backend association revoke를 bounded request로 실행한다.
  4. 제품 정책에 따라 native unsubscribe()를 best effort로 실행한다.
  5. old association tag로 표시한 notification을 bounded getNotifications()로 찾아 best-effort close한다.
  6. 새 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를 닫지 않는다.

같은 associationEpochREVOKED는 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/5xx cleanup/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 허용하는 두 형태

  1. freshness polling: visible 화면의 server-state query를 낮은 빈도로 conditional refetch한다.
  2. 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 LogicalExecutionBudgetmaxAttempts=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
  • setTimeout completion chaining으로 single-flight를 보장한다.
  • interval tick이 이전 request와 겹치지 않는다.
  • success cadence와 failure backoff를 구분한다.
  • Poll maxAttempts의 한 attempt는 registered REST operation의 한 logical completion이자 한 physical request다. credential recovery, decode와 mapping 시간도 lease maxElapsed에 포함하며 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의 bounded Retry-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가 LIVEPOLLING/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와 자유 문구를 추가하는 예외가 아니다.

fenceGenerationsessionBindingEpoch은 §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_changed
  • realtime_connect_attempted
  • realtime_connect_finished
  • realtime_connection_closed
  • realtime_heartbeat_observed
  • realtime_event_received
  • realtime_event_rejected
  • realtime_event_applied
  • realtime_gap_detected
  • realtime_resync_finished
  • realtime_queue_pressure
  • realtime_fallback_changed

polling:

  • bounded_poll_lease_started
  • bounded_poll_attempt_finished
  • bounded_poll_unchanged
  • bounded_poll_rate_limited
  • bounded_poll_lease_stopped

push:

  • web_push_permission_finished
  • web_push_registration_finished
  • web_push_subscription_rotated
  • web_push_hint_processed
  • web_push_notification_finished
  • web_push_click_dispatched
  • web_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/bufferedAmount FIFO 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 REVOKED tombstone resurrection 거절
  • exact revoked association purge와 concurrent newer-owner revision CAS
  • malformed/oversized/expired/wrong-scope hint
  • worker waitUntil, handler deadline, pushsubscriptionchange lifecycle abort와 non-cooperative client enumeration
  • safe localized notification, persisted typed click route와 old-tag cleanup
  • declarative web_push: 8030 payload rejection
  • provider 404/410/429/5xx mapping

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 setInterval polling
  • 선택 전 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

필수 외부 증거가 없으면 PromotionEvidenceMISSING | 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:

  1. 새 traffic admission을 닫는다.
  2. active connection/poll/push registration 작업을 drain한다.
  3. stale/focus/manual refresh fallback을 노출한다.
  4. backend publisher/replay/subscription compatibility window를 유지한다.
  5. frontend composition과 dependency를 제거한다.
  6. production bundle/worker에서 source 부재를 검증한다.

SSE에서 WebSocket으로, 또는 그 반대로 즉석 rollback하지 않는다. protocol semantics가 다른 경우 기존 HTTP/stale fallback으로 먼저 안전하게 내린다.

17. 제거 가능성

17.1 제거 순서

foreground:

  1. traffic admission DISABLED, connection lifecycle DRAINING
  2. logical subscription revoke
  3. Poll lease와 reconnect/backoff timer abort
  4. SSE reader/EventSource 또는 WebSocket close
  5. lifecycle/online listener 제거
  6. cursor/dedupe/queue memory 폐기
  7. composition/registry 제거
  8. adapter, port, test와 dependency 제거

Web Push:

  1. 새 opt-in과 server send admission 중지
  2. durable fence generation rotate + local REVOKED를 같은 transaction에 commit
  3. backend account association revoke
  4. owned notification bounded close와 native unsubscribe policy 실행
  5. association fence store exact purge
  6. push/notification handler를 worker에서 제거
  7. worker update/drain과 old client 호환 확인
  8. notification registry, provider adapter와 key config 제거
  9. 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. 관련 자료

저장소 내부:

공식 기준: